본문 바로가기

Project

[Project] Lv_5 스케줄 프로젝트(심화)

 


 

요구사항

Validation을 활용해 다양한 예외처리를 적용해 봅니다. → 1주차 Bean Validation 참고!

정해진 예외처리 항목이 있는것이 아닌 프로젝트를 분석하고 예외사항을 지정해 봅니다.

  • ✅ Ex) 할일 제목은 10글자 이내, 유저명은 4글자 이내
  • ✅ @Pattern을 사용해서 회원 가입 Email 데이터 검증 등
    • ✅ 정규표현식을 적용하되, 정규표현식을 어떻게 쓰는지 몰두하지 말 것!
    • ✅ 검색해서 나오는 것을 적용하는 것으로 충분!

 


 

요구 구현

User (Entity)

더보기
@Getter
@Entity
@Table(name = "user")
public class User extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false, unique = true)
    private String email;

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    public User() {

    }

    public void updateUsername(String username) {
        this.username = username;
    }

    public void updateEmail(String email) {
        this.email = email;
    }
}
  • `@Id`, `@GeneratedValue(strategy = GenerationType.IDENTITY)` : Java의 JPA(Java Persistence API)에서 Entity의 기본 키(Primary Key)를 정의할 때 사용하는 어노테이션입니다.
    • `@Id` : 해당 필드가 Entity의 기본 키(Primary key)임을 나타냅니다.
    • `@GeneratedValue(strategy = GenerationType.IDENTITY)` : 기본 키의 값을 자동으로 생성하도록 JPA에 지시합니다. (INSERT 할 때 기본 키 값을 따로 지정하지 않아도 DB가 알아서 증가된 값을 넣어줍니다.)
  • `@Colmun` : JPA에서 Entity 클래스의 필드를 데이터베이스 테이블의 열(Column)에 매핑할 때 사용됩니다. 생략해도 기본 동작은 하지만, 세부 설정을 하고 싶을 때 사용합니다.
    • nullable = false : `NULL` 허용하지 않음
    • length = 100 : 문자열 길이 100 (기본 255)
    • unique = true : 유일한 값으로 설정

 

 

 

CreateUserRequestDto

더보기
@Getter
@AllArgsConstructor
public class CreateUserRequestDto {
    @NotNull
    @Size(max = 100)
    private final String username;

    @NotNull
    @Email(message = "이메일 형식이 올바르지 않습니다")
    private final String email;

    @NotNull
    @Pattern(
            regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&#])[A-Za-z\\d@$!%*?&#]{10,}$",
            message = "비밀번호는 10자 이상이며, 대소문자, 숫자, 특수문자를 포함해야 합니다."
    )
    private final String password;
}
  • `@NotNull` : null 값을 받지 않는 유효선 검사 어노테이션
  • `@Size(max = 100)` : 최대 길이를 100으로 받는 유효선 검사 어노테이션
  • `@Pattern(regexp = ...)` : 비밀번호 10자리 이상, 대소문자 필요, 숫자, 특수문자를 포함해야하는 것을 나타내는 정규식 어노테이션

 

 

UpdateUserRequestDto

더보기
@Getter
@AllArgsConstructor
public class UpdateUserRequestDto {
    @NotNull
    @Size(max = 100)
    private final String username;

    @NotNull
    @Email(message = "잘못된 이메일 형식입니다.")
    private final String email;
}
  • `@NotNull` : null 값을 받지 않는 유효선 검사 어노테이션
  • `@Size(max = 100)` : 최대 길이를 100으로 받는 유효선 검사 어노테이션
  • `@Email` : 이메일 형식으로 받는 유효성 검사 어노테이션

 

 

DeleteUserRequestDto

더보기
@Getter
@AllArgsConstructor
public class DeleteUserRequestDto {

    @NotNull
    @Pattern(
            regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&#])[A-Za-z\\d@$!%*?&#]{10,}$",
            message = "비밀번호는 10자 이상이며, 대소문자, 숫자, 특수문자를 포함해야 합니다."
    )
    private final String password;
}
  • `@NotNull` : null 값을 받지 않는 유효선 검사 어노테이션
  • `@Pattern(regexp = ...)` : 비밀번호 10자리 이상, 대소문자 필요, 숫자, 특수문자를 포함해야하는 것을 나타내는 정규식 어노테이션

 


 

Schedule (Entity)

더보기
@Entity
@Table(name = "schedule")
@Getter
public class Schedule extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private Long userId;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false, length = 100)
    private String content;

    public Schedule() {}

    public Schedule(Long userId, String title, String content) {
        this.userId = userId;
        this.title = title;
        this.content = content;
    }

    public void updateTitle(String title) {
        this.title = title;
    }

    public void updateContent(String content) {
        this.content = content;
    }
}

 

 

 

 

CreateScheduleRequestDto

더보기
@Getter
@AllArgsConstructor
public class CreateScheduleRequestDto {
    @NotNull
    private Long userId;

    @Size(min = 5)
    @NotNull
    private String title;

    @Size(max = 100)
    @NotNull
    private String content;
}

 

 

UpdateScheduleRequestDto

더보기
@Getter
@AllArgsConstructor
public class UpdateScheduleRequestDto {
    @NotNull
    @Size(max = 10)
    private String password;

    @Size(min = 5)
    @NotNull
    private String title;

    @Max(100)
    @NotNull
    private String content;
}

 


 

LoginRequestDto

더보기
@Getter
@AllArgsConstructor
public class LoginRequestDto {
    @NotNull
    @Email(message = "이메일 형식이 올바르지 않습니다")
    private String email;

    @NotNull
    @Pattern(
            regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&#])[A-Za-z\\d@$!%*?&#]{10,}$",
            message = "비밀번호는 10자 이상이며, 대소문자, 숫자, 특수문자를 포함해야 합니다."
    )
    private String password;
}

 

 

트러블 슈팅

(문제)

  • 동일한 Email을 갖는 2명의 유저를 생성하였습니다.

 

  • 해당 이메일(`test@naver.com`)로 로그인 시, `Query did not return a unique result: 2 results were returned`라는 메시지와 함께 오류가 발생합니다.

 

  • 서버 오류에서는 `NonUniqueResultException`이 발생하였습니다.
  • `NonUniqueResultException` : 결과가 정확히 하나여야 하는 쿼리에서 여러 개의 결과가 나올 때 발생하는 예외입니다.

 

 

(원인)

  • 로그인을 진행하는 로직에서 유저 이메일을 검증하고 하나의 유저를 리턴 받을 때, 위 코드와 같이 실행됩니다.
  • 하나의 유저만 받아야 하는 상황에서 Email이 동일한 두 유저가 반환되기 때문에 발생하는 오류입니다.

 

 

(해결)

해결 전

 

해결 후

  • 고유한 이메일을 갖도록 `@Column(unique = true)`를 통해 오류를 해결하였습니다.

 


 

회고

유효성 검사 어노테이션(`@NotNull`, `@Email`, `@Size` 등)과 DB 제약 어노테이션(`@Column`)을 같이 사용하는 이유와 언제 사용해야 하는지 궁금했습니다.

 

유효성 검사 어노테이션은 DTO에서, DB 제약 어노테이션은 Entity에서 하는 것이 관례라고합니다. 그 이유는 다음과 같습니다.

 

1. 책임 분리(Separation of Concerns)

클래스 책임(역할)
DTO (Data Transfer Object) 클라이언트의 요청/응답 데이터를 담고 유효성 검사를 수행
Entity (도메인 모델) DB와 매핑되고, 비즈니스 로직을 담는 핵심 도메인 모델
  • DTO는 주로 외부(프론트엔드 등)에서 넘어온 입력값을 검증하는 데 초점
  • Entity는 DB와의 매핑, 비즈니스 규칙 적용에 초점

 

2. 유효성 검증은 컨트롤러/서비스 단에서 처리해야 사용자에게 피드백이 빠릅니다.

 

  • Entity에 유효성 검사 어노테이션을 넣으면, Entity가 생성될 때만 유효성 검사가 동작합니다.
  • 하지만 보통은 사용자의 입력값이 잘못됐을 때, 컨트롤러 진입 전에 에러를 주는 것 이 더 적절합니다.

 

3. Entity는 외부 변경에 유연해야 합니다.

 

  • 프론트엔드 요구로 입력 필드가 바뀌면, DTO만 수정하면 됩니다.
  • 만약 Entity에 직접 유효성 검사를 넣었다면, 도메인 로직과 DB 매핑까지 건드려야 하므로 리스크가 큽니다.

 

 

→ DTO는 자주 바뀌고, Entity는 자주 바뀌지 않아야 유지보수가 쉽습니다.