프로젝트를 회고하면서 많은 부분을 놓치며 완성했다는 사실을 깨달았다. 만들어지지 않은 기능들도 있었고, 시간에 쫓겨 완성만을 위해 코드를 작성했다는 것이 눈에 보였다. 물론 그 당시에는 나의 최선이었음을 알지만, 회고를 하면서 고칠 수 있는 부분은 고치고 공부도 해보자는 생각이 들어서 짧게나마 글을 작성해놓는다.
- @Builder 사용법
@Builder 어노테이션은 객체를 생성할 때 사용하는 패턴으로, 생성자를 통해 객체를 생성하는 것보다 가독성이 좋으며 순서에 상관없이 값을 삽입할 수 있다. 회고 전 프로젝트에서는 JPA 엔티티 클래스에서 @Builder, @NoArgsConstructor와 @AllArgsConstructor을 같이 사용했다. @NoArgsConstructor의 생성자에 접근 제어를 하게되면, @Builder가 모든 필드가 포함된 기본 생성자를 만들 수 없어 오류가 난다. 이를 해결하기 위해 @AllArgsConstructor 어노테이션을 함께 사용했다.
@Entity
@Builder
@Table(name = "PROJECT",
indexes = {
@Index(name = "idx_project_num", columnList = "project_num"),
@Index(name ="idx_user_id", columnList = "user_id"),
@Index(name="idx_written_date", columnList = "written_date")
})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Project {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "project_id", nullable = false, updatable = false, length = 36)
private String projectId; // 프로젝트 UID
@Column(name = "user_id", nullable = false, updatable = false)
private String userId; // 프로젝트 작성자아이디
}
하지만 @AllArgsConstructor의 경우 모든 필드의 생성자를 자동으로 생성하는데, 인스턴스 멤버의 선언 순서에 영향을 받는다. 변수의 순서가 변경될 때 동일한 타입일 경우에는 컴파일 에러는 발생하지 않지만, 원하지 않는 값이 들어갈 수 있는 위험이 있다는 설명을 읽었다. 그래서 @AllArgsConstructor를 이용해서 생성자를 정의하는 방법보다, 클래스 내에 생성자를 정의하여 @Builder를 사용하는 방안으로 코드를 변경했다.
@Entity
@Table(name = "PROJECT",
indexes = {
@Index(name = "idx_project_num", columnList = "project_num"),
@Index(name ="idx_user_id", columnList = "user_id"),
@Index(name="idx_written_date", columnList = "written_date")
})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Project {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "project_id", nullable = false, updatable = false, length = 36)
private String projectId; // 프로젝트 UID
@Column(name = "user_id", nullable = false, updatable = false)
private String userId; // 프로젝트 작성자아이디
@Builder
public Project(String projectId, String userId) {
this.projectId = projectId;
this.userId = userId;
}
}
- DTO에서의 @getter, @setter 사용
Entity의 경우 @setter 사용을 지양하는데, DTO의 경우에는 @setter 사용이 괜찮은걸까란 궁금증에 검색을 해봤다. Entity의 경우 비즈니스 로직을 포함하고 있어, 불필요한 값 변경 방지를 위해 @setter 사용을 지양한다. 반면 DTO는 단순히 데이터를 전달하는 용도로 사용되므로 @setter를 사용해도 무방하다.
DTO에서 @getter를 쓰지않으면 오류가 나는 이유는 뭘까란 질문도 찾아봤는데, HTTP 요청을 받을 때 @RequestBody로 전달된 JSON데이터를 ObjectMapper가 DTO 객체로 변환할 때 필드 값을 읽어야 하기 때문이다.
- DTO에서의 유효성 검사
프로젝트를 회고하다보니 유효성 검사를 시행하지 않았다는 사실을 알았다. 어디서 유효성검사를 시행해야하는가란 궁금증이 생겼는데, DTO에서 기본적인 유효성 검사를 한 후에, Service 측에서 복잡한 유효성 검사를 수행해야 한다는 글을 읽었다. 클라이언트로부터 잘못된 데이터가 서버에 도달하는 것을 차단하는 목적으로 DTO에서 유효성 검사를 수행한다. 나는 프로젝트를 하면서 클라이언트로부터 받는 데이터를 inDTO로, 서버에서 클라이언트에게 전달하는 데이터를 outDTO로 설정했다. 따라서 inDTO에서만 유효성 검사를 수행했다.
@Getter
@Setter
public class UserInDTO {
@Email(message = "올바른 이메일 형식을 입력하세요.")
@NotBlank(message = "이메일은 필수 입력값입니다.")
private String email;
// 영문자 최소 1개 이상, 숫자 최소 1개 이상, 특수문자(!@#$%^&*_-) 최소 1개 이상, 영문자, 숫자, 특수문자로 이루어진 8~20자 문자열
@Pattern(
regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^&*_-])[a-zA-Z\\d!@#$%^&*_-]{8,20}$",
message = "비밀번호는 영문자, 숫자, 특수문자(!@#$%^&*_-)를 포함한 8~20자리여야 합니다."
)
private String password;
@NotBlank(message = "이름은 필수 입력값입니다.")
@Size(max = 50, message = "이름은 최대 50자까지 입력 가능합니다.")
private String name;
}
- @AssertTrue와 @AssertFalse
@AssertTrue는 특정 조건이 참일 때, @AssertFalse는 특정 조건이 거짓일 때 데이터를 처리하도록 하는 어노테이션이다. Komofunding의 QnAInDTO 클래스의 title은 qnaCategory에 따라서 반드시 작성되어야하는 규칙을 설정해야했다. 회고 전에는 Service에서 해당 검증을 시행했다. 이렇게 될 경우 검증 로직이 여러 곳에 분산되기도 하고, Service의 코드가 복잡해지는 문제가 생겼다. 그래서 @AssertTrue를 사용하여 프로젝트 내 댓글 문의(COMMENT)일 경우네는 true를 반환하고, 1:1문의(QUESTION)일 경우에는 제목을 작성하도록 설정하였다.
@Getter
@Setter
public class QnAInDTO {
private String userId; // 작성자 userId
@NotNull
private QnaCategory qnaCategory; // 문의글 카테고리 (프로젝트 댓글에 적을때는 COMMENT, 1:1문의는 QUESTION)
@Size(max = 100, message = "문의글 제목은 최대 100자까지 입력 가능합니다.")
private String title; // 문의글 제목 ( 댓글일 경우에는 null 값 허용 )
// QUESTION이 아니라면 true반환, QUESTION이면 title 작성
@AssertTrue(message = "1:1 문의글 제목을 작성해주세요.")
private boolean isTitleValid() {
return qnaCategory != QnaCategory.QUESTION || (title != null && !title.trim().isEmpty());
}
@NotBlank(message = "문의글을 작성해주세요.")
@Size(min = 10, max = 1000, message = "문의글 내용은 최소 10자 이상, 최대 1000자까지 입력 가능합니다.")
private String questionComment; // 문의글 내용
private String answerUserId; //답변자 userId
@Size(max = 1000, message = "답변 내용은 최대 1000자까지 입력 가능합니다.")
private String answer; // 문의 답변
}
- @NotNull, @NotEmpty, @NotBlank
* NotNull : null을 허용하지않음, "" 나 " "는 허용함 - null 값일 때를 확인
* NotBlank : null과 "", " "를 허용하지 않음 - String 타입만 사용가능
* NotEmpty : null과 ""를 허용하지않음, " "는 허용 - 비어있는 상태를 확인
- Page와 Slice
프로젝트를 진행하는 당시에는 백엔드에서의 페이징 처리를 고려하지 않고, 프론트에서 전체 데이터를 받아 페이징 처리를 하는 방식으로 설계를 진행했다. 회고를 통해 이를 다시 살펴보니, 백엔드에서 데이터를 한 번에 전송하면 네트워크를 통해 전송되는 데이터의 양이 증가하게 되면서 응답 시간이 늘어나는 문제가 발생할 수 있다는 생각이 들었다. 이를 개선하기 위해서 백엔드에서 페이징 처리를 수행해서 필요한 데이터만 전송하는 방식으로 변경했다.
Page와 Slice를 통해서 변경했는데, Page와 Slice의 경우 pageable 객체를 사용하여 페이징과 정렬 조건을 전달한다. Page는 전체 페이지, 전체 데이터 수를 가지고 있기 때문에 총 페이지 번호가 필요한 게시판에서 사용했고, Slice의 경우에는 다음 페이지만 확인할 수 있어 더보기 기능이 있는 게시판에서 사용했다.
// ProjectRepository
@Repository
public interface ProjectRepository extends JpaRepository<Project, String> {
Page<Project> findByProjectCategory(ProjectCategory category, Pageable pageable);
Slice<Project> findSliceByProjectCategory(ProjectCategory category, Pageable pageable);
}
- Exception 처리
프로젝트 Exception에 대한 처리가 Service와 Controller에서 이루어졌다. 이러다보니 코드가 많아져 지저분하기도하고, 같은 내용을 똑같이 작성을 해야하는 일도 발생했다. 이러한 점을 개선하기 위해서 @ControllerAdvice와 @ExceptionHandler를 사용하여 예외 처리 로직을 한 곳에서 관리하고, 보다 깔끔하게 코드를 정리해보고자 하였다.
1. 에러코드를 Enum 클래스를 통하여 상수화
public enum ErrorCode {
// 공통 에러
INVALID_INPUT_VALUE(400, "COMMON-ERR-001", "잘못된 입력 형식입니다."),
INTERNAL_SERVER_ERROR(500, "COMMON-ERR-002", "서버 오류가 발생하였습니다."),
TIMEOUT_ERROR(408, "COMMON-ERR-003", "요청 시간이 초과되었습니다."),
BAD_REQUEST(400, "COMMON-ERR-004", "잘못된 요청입니다."),
RESOURCE_NOT_FOUND(404, "COMMON-ERR-005", "요청된 리소스를 찾을 수 없습니다."),
UNSUPPORTED_MEDIA_TYPE(415, "COMMON-ERR-006", "지원되지 않는 파일 형식입니다."),
// 계정 관련 에러
UNAUTHORIZED(401, "ACCOUNT-ERR-001", "인증에 실패하였습니다."),
ACCOUNT_NOT_FOUND(404, "ACCOUNT-ERR-002", "사용자를 찾을 수 없습니다."),
ROLE_NOT_EXISTS(403, "ACCOUNT-ERR-003", "사용자 권한이 없습니다."),
DUPLICATE_EMAIL(400, "ACCOUNT-ERR-OO4", "이미 존재하는 이메일입니다."),
PASSWORD_MISMATCH(400, "ACCOUNT-ERR-OO5", "비밀번호가 일치하지 않습니다."),
USER_INACTIVE(403, "ACCOUNT-ERR-OO6", "비활성화된 사용자입니다."),
private final int status;
private final String code;
private final String message;
ErrorCode(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
public int getStatus(){
return status;
}
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
}
2. Exception 발생 시 응답 에러 클래스 작성
public record ErrorResponse(String code, String message) {}
3. CustomException 클래스 작성 : ErrorCode를 처리하는 기본 클래스 역할
@Getter
@RequiredArgsConstructor
public class CustomException extends RuntimeException{
private final ErrorCode errorCode;
}
4. @ControllerAdvice와 @ExceptionHandler를 사용하여 CustomException이 발생했을 때 응답반환
@ControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(CustomException.class)
public ResponseEntity<CustomErrorResponse> handleCustomException(CustomException ex) {
CustomErrorCode errorCode = ex.getErrorCode(); // CustomException에서 ErrorCode를 추출
CustomErrorResponse errorResponse = new CustomErrorResponse(
errorCode.getCode(),
errorCode.getMessage()
);
return ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus()))
.body(errorResponse);
}
}
5. 사용
// 유저 찾기
User user = userRepository.findById(qna.getUserId())
.orElseThrow(() -> new CustomException(ACCOUNT_NOT_FOUND));
- 필요한 DTO 생성
프로젝트 내에 후원자 리스트를 포함하는 부분이 있다. 이전 프로젝트를 하면서는 ProjectOutDTO에 UserOutDTO형식을 사용하여 데이터를 전달했다. 이렇게 될 경우 민감한 정보가 그대로 들어갈 수도 있고 필요없는 정보도 불러와 성능저하를 유발시킬 수 있을거란 생각이 들었다. 이런 점을 개선하고자 ProjectOutDTO에서 후원자 정보를 삭제하고 SupporterDTO를 따로 분류했다.
// 프로젝트 후원자 정보 DTO
@Getter
@Builder
public class SupporterDTO {
// 유저관련
private String userNum;
private String nickName;
private String name;
private String email;
private String phoneNumber;
// 결제관련
private List<ItemDTO> items;
private Long paidAmount;
private LocalDateTime paymentDate;
// 주소관련
private String senderName;
private String shippingName;
private String shippingPhone;
private String shippingAddress;
}
'다이어리 > 소소한 공부' 카테고리의 다른 글
프로젝트 회고 - 02. Pitchplay (0) | 2025.02.11 |
---|