Skip to content

Conversation

ghkdgus29
Copy link
Contributor

@ghkdgus29 ghkdgus29 commented Apr 5, 2025

✨ 요약

관심 키워드 테이블 생성

키워드가 유니크하게 다대다로 해야할까?

Table subscriber {
  subscriber_id bigint
  email varchar(255)
  subscribed bit(1)

  created_at datetime
  updated_at datatime
  deleted_at datetime 
}
// 이미 존재하는 테이블 

Table subscriber_interest {
  id bigint
  subscriber_id bigint 
  interest_id bigint

  created_at datetime
  updated_at datatime
  deleted_at datetime 
}

Table interest {
  id bigint
  keyword varchar(255)

  created_at datetime
  updated_at datetime
  deleted_at datetime
}

Ref: subscriber.subscriber_id < subscriber_interest.subscriber_id
Ref: interest.id < subscriber_interest.interest_id
 

[dbdiagrm.io](http://dbdiagrm.io) script

image

CREATE TABLE interest (
  id BIGINT PRIMARY KEY,
  keyword VARCHAR(255),
  created_at DATETIME,
  updated_at DATETIME,
  deleted_at DATETIME
);

CREATE TABLE subscriber_interest (
  id BIGINT PRIMARY KEY,
  subscriber_id BIGINT,
  interest_id BIGINT,
  created_at DATETIME,
  updated_at DATETIME,
  deleted_at DATETIME,
  FOREIGN KEY (subscriber_id) REFERENCES subscriber(subscriber_id),
  FOREIGN KEY (interest_id) REFERENCES interest(id)
);

mysql DDL

키워드 글자가 중요한 것이니, 편의를 위해 일대다로 할까?

 Table subscriber {
  subscriber_id bigint
  email varchar(255)
  subscribed bit(1)

  created_at datetime
  updated_at datatime
  deleted_at datetime 
}
// 이전에 있던 테이블

// Table subscriber_interest {
//   id bigint
//   subscriber_id bigint 
//   interest_id bigint

//   created_at datetime
//   updated_at datatime
//   deleted_at datetime 
// }

Table interest {
  id bigint
  keyword varchar(255)
  subscriber_id bigint 

  created_at datetime
  updated_at datetime
  deleted_at datetime
}

Ref: subscriber.subscriber_id < interest.subscriber_id

dbdiagram.io

image

CREATE TABLE interest (
  id BIGINT PRIMARY KEY,
  keyword VARCHAR(255),
  subscriber_id BIGINT,
  created_at DATETIME,
  updated_at DATETIME,
  deleted_at DATETIME,
  FOREIGN KEY (subscriber_id) REFERENCES subscriber(subscriber_id)
);

mysql DDL

  • 결론: 일단은 개발 편의를 위해 일대다로 진행
    • 키워드 글자만 중요하다고 판단하였기 때문
    • 키워드 글자를 가지고 API 호출 시 쿼리 스트링으로 사용

엔티티 매핑

[How does JPA orphanRemoval=true differ from the ON DELETE CASCADE DML clause](https://stackoverflow.com/questions/4329577/how-does-jpa-orphanremoval-true-differ-from-the-on-delete-cascade-dml-clause)

  • Subscriber
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "SUBSCRIBER")
@Builder
@AllArgsConstructor
public class Subscriber extends BaseTimeEntity {

    // ...

    @OneToMany(mappedBy = "subscriber", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Interest> interests = new ArrayList<>();

    public Subscriber(String email, List<String> keywords) {
        this.email = email;

        keywords.stream()
                .map(Interest::new)
                .forEach(this::add);
    }

    public void add(Interest interest) {
        interests.add(interest);
        interest.setSubscriber(this);
    }
  • Interest
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Builder
@AllArgsConstructor
public class Interest extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String keyword;

    @ManyToOne
    @JoinColumn(name = "subscriber_id")
    private Subscriber subscriber;

    public void setSubscriber(Subscriber subscriber) {
        this.subscriber = subscriber;
    }
}

기능 추가

  • API request 변경
@NoArgsConstructor
@Getter
public class SubscribeRequestDto {
    private String email;

    @Size(min=1, max = 3, message = "You must provide 1 to 3 keywords.")
    private List<
            @NotBlank(message = "Each keyword must not be blank.")
            @Size(min = 2, max = 20, message = "Keyword must be between 2 and 20 characters.")
                    String> keywords;
    // keywords List 추가 
}
  • service에 저장 로직 수정
    @Override
    @Transactional
    public void subscribe(String email, List<String> keywords) {
        // 이메일 중복 체크
        subscriberRepository.findByEmail(email).ifPresent(subscriber -> {
            throw new IllegalArgumentException("이미 구독된 이메일입니다.");
        });
        emailVerifyCheck(email);

        // 관심 키워드와 함께 새 구독자 저장
        Subscriber subscriber = new Subscriber(email, keywords);
        subscriberRepository.save(subscriber);
    }

신규 구독 시에만 타는 로직이라 판단해서, 현재 subscirber가 갖고있는 keyword 개수 안셈

  • keyword 개수는 당연히 0개니까 (new)

기능 구현 테스트

email: “[email protected]

  • API request 바뀐대로 잘 받는지
  • 3개 이상 키워드는 예외 던지는 지
    • 각 키워드의 글자수 제한이 잘 걸리는지 2~20자
  • 빈 키워드도 예외 던지는지
  • 키워드 중복 제거 잘 되는지
  • 구독자의 관심 키워드가 연관관계로 잘 들어가서 DB에 저장되었는지

추가 커밋 이유

image

  • 한 글자는 사용 못하도록 검증 애노테이션 달았지만, “ a “ 와 같이 빈칸과 같이 오면 통과함
  • 내가 원하는 궁극적인 검증은 입력으로 들어온 것들의 중복을 제거하고, trim을 모두 적용한 뒤 검증 로직을 적용했으면 좋겠음

해결

  • 직접 Validator 구현

  • validator

public class KeywordsValidator implements ConstraintValidator<ValidKeywords, List<String>> {

    private int minSize;
    private int maxSize;
    private int keywordMinLength;
    private int keywordMaxLength;

    @Override
    public void initialize(ValidKeywords constraintAnnotation) {
        this.minSize = constraintAnnotation.minSize();
        this.maxSize = constraintAnnotation.maxSize();
        this.keywordMinLength = constraintAnnotation.keywordMinLength();
        this.keywordMaxLength = constraintAnnotation.keywordMaxLength();
    }

    @Override
    public boolean isValid(List<String> value, ConstraintValidatorContext constraintValidatorContext) {
        if (value == null) return false;

        List<String > processed = value.stream()
                .map(String::trim)
                .filter(StringUtils::isNotBlank)
                .distinct()
                .collect(Collectors.toList());

        if (processed.size() < minSize || processed.size() > maxSize) {
            return false;
        }

        return processed.stream()
                .allMatch(s -> s.length() >= keywordMinLength && s.length() <= keywordMaxLength);
    }
}

원래 리스트를 순회한김에 변환하여 반환하려고 했음

그러나, 검증용 애노테이션이 실제 필드값을 변환시킨다는 것이 추후 이해하기 힘들 것 같아 해당 부분 제거

실제로 많아봐야 3개정도라서 dto에서 getter를 호출할 때 중복값 제거 및 trim을 해줘도 된다고 생각했음

  • valid annotation
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = KeywordsValidator.class)
public @interface ValidKeywords {

    String message() default "Invalid keywords.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    int minSize() default 1;
    int maxSize() default 3;
    int keywordMinLength() default 1;
    int keywordMaxLength() default 200;
}
  • API request dto
@NoArgsConstructor
public class SubscribeRequestDto {
    @Getter
    private String email;

    @ValidKeywords(minSize = 1, maxSize = 3, keywordMinLength = 2, keywordMaxLength = 20,
            message = "Keywords must be 1~3, each 2~20 characters and unique.")
    private List<String> keywords;

    // 중복 제거
    public List<String> getKeywords() {
        return keywords.stream()
                .map(String::trim)
                .filter(StringUtils::isNotBlank)
                .distinct()
                .collect(Collectors.toList());
    }
}
  • 체크리스트
  • “ab”, “ b” 실패
  • “ab”, “ ab ” get 결과는 “ab” 한개
  • “abc”, “ abc ”, “abc”, “bb”, “cc” 가능 (중복제거를 통해 총 3개)

추가로 할 것

  • 현재 API request 검증에 걸리는 경우, 예외 메시지(argumentNotValidException) 그냥 던지고 있는데 원하는 에러 형식이 있으면 맞춰서 드리겠습니다.

image



😎 해결한 이슈

[✨ feat] 관심 키워드 알림 서비스 #48



궁금증

  • UserService API 스펙이 바뀌었는데, Swagger 설정 따로 해줘야하는지?
  • API 명세서는 어디서 볼 수 있는지?
  • POSTMAN 연동되는지
  • LIVE RELOAD 같은 거 해놓으신건지

@ghkdgus29 ghkdgus29 added backend ✨ feature 기능 추가 labels Apr 5, 2025
@ghkdgus29 ghkdgus29 self-assigned this Apr 5, 2025
@ghkdgus29 ghkdgus29 linked an issue Apr 5, 2025 that may be closed by this pull request
2 tasks
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Comments suppressed due to low confidence (2)

src/main/java/com/example/userservice/domain/subscriber/service/impl/SubscriberServiceImpl.java:27

  • Consider adding a null check or additional validation for the 'keywords' parameter here to ensure it is not null, even though the DTO validation should typically handle this.
public void subscribe(String email, List<String> keywords) {

src/main/java/com/example/userservice/domain/subscriber/entity/Subscriber.java:40

  • [nitpick] Consider trimming each keyword before creating the Interest object to ensure that accidental leading or trailing whitespace does not affect uniqueness or behavior.
keywords.stream().distinct().map(Interest::new).forEach(this::add);

Copy link

github-actions bot commented Apr 5, 2025

Unit Test Results

15 tests  ±0   15 ✔️ ±0   19s ⏱️ -1s
  4 suites ±0     0 💤 ±0 
  4 files   ±0     0 ±0 

Results for commit cb2b4c9. ± Comparison against base commit 1201c56.

♻️ This comment has been updated with latest results.

Copy link
Member

@minwoo1999 minwoo1999 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM~

Comment on lines 26 to 42
public boolean isValid(List<String> value, ConstraintValidatorContext constraintValidatorContext) {
if (value == null) return false;

List<String > processed = value.stream()
.map(String::trim)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());

if (processed.size() < minSize || processed.size() > maxSize) {
return false;
}

return processed.stream()
.allMatch(s -> s.length() >= keywordMinLength && s.length() <= keywordMaxLength);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

특수기호에 대한 validation은 하지않아도 될까요?!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤거 검증하면 좋을까요?
지금 당장은 생각나는게 없어서..

필요한 validation 말씀해주시면 바로 추가할게용 ㅎㅎ

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 제 생각엔 그냥 static한 정규표현식으로 특수기호체크하는 로직만 추가되면 좋을 것 같네요~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pattern.compile("^[a-zA-Z0-9가-힣 ]+$");

영문, 한글, 숫자, 띄어쓰기만 가능하도록 했습니당

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GOOD~ 머지하셔도 될 것 같아요 approve 했습니다

Copy link
Member

@agape1225 agape1225 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

늦어서 죄송합니다 ㅠㅠ 확인 했습니다! 👏🏻

@ghkdgus29 ghkdgus29 merged commit 84187f8 into develop Apr 6, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[✨ feat] 관심 키워드 알림 서비스
3 participants