2024.10.07.(월)

5주차

질문 CRUD를 작성하는 시간!

질문 도메인 작성

import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Table(name = "questions")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Question extends BaseEntity {

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

  @Column(columnDefinition = "TEXT", nullable = false)
  private String subject;

  @Column(columnDefinition = "TEXT", nullable = false)
  private String content;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "author_id")
  private User author;

  // TODO: 질문 리스트 매핑

  public Question(
    String subject,
    String content,
    User author
  ) {
    // id는 JPA가 자동으로 생성하여 DB에 저장
    this.subject = subject;
    this.content = content;
    this.author = author;
  }
}

Entity에 protected 생성자를 만드는 이유

  • NoArgsConstructor(access = AccessLevel.PROTECTED)
  • 기본 생성자의 접근제어를 protected로 설정하여 무분별한 객체 생성에 대해 체크하기 위함이다.
  • private가 더 안전하지 않나요?
    • JPA 표준 스펙에 디폴트 생성자가 있어야 하므로 private이 될 수 없다.
    • JPA가 프록시 기술을 사용할 때 Jpa hibernate가 객체를 생성하는데, private을 사용하면 이러한 방식이 수행될 수 없다.

빌더 패턴 (Builder Pattern)

  • 해당 도메인의 경우, 인스턴스 필드가 많지도 않아서 빌더 패턴이 아닌 생성자를 사용하였다. 그러나 빌더 패턴이 익숙하다면 빌더 패턴을 사용해도 된다.

  • 빌더 패턴이란?

    • 별도의 Builder 클래스를 만들어 메서드를 통해 순차적으로 값을 입력받은 후, 최종적으로 build() 메소드를 통해 하나의 인스턴스를 생성하여 반환하는 패턴이다.
      • 별도의 Builder 클래스를 만들어서 사용해도 되지만, Lombok의 @Builder 어노테이션을 사용하자.
    • 점층적 생성자 패턴과 자바 빈즈(Java Beans) 패턴의 장점만 취합한 패턴이라고 볼 수 있다.
    • 간단한 예시
    Question question = new Question.Builder()
                .subject("질문 있어용")
                .content("모르게써용")
                .author("하하")
                .build();
    
  • 빌더 패턴의 장점

    • 생성자 오버로딩 열거를 하지 않아도 된다.
    • 데이터의 순서에 제약을 받지 않고 객체를 만들 수 있다. -> 생성자 파라미터 순서를 파악하지 않아도 된다.
    • 객체의 생성 과정을 일관된 프로세스로 표현할 수 있다.
      • 생성자 방식 대신 빌더 패턴을 사용하는 이유이다.
      • 생성자 방식으로 객체를 생성하면 파라미터가 많아질수록 가독성이 심히 안좋아진다.
      • 빌더 패턴을 적용하면 어떤 데이터에 어떤 값이 들어가는지 쉽게 파악할 수 있다. -> 파라미터에 잘못된 값을 넣는 실수도 방지할 수 있다.
    • 디폴트 매개변수 생략을 간접적으로 지원한다.
      • 자바는 메소드에 대한 디폴트 매개변수 생략을 지원하지 않는다.
        • 참고로 코틀린의 경우 디폴트 파라미터의 값을 생략할 수 있다.
      • 빌더 패턴은 디폴트 필드를 제외하고 빌더를 구성하여 객체를 생성할 수 있다.
    Question question = new Question.Builder()
         .subject("질문 있어용")
         .content("모르겠어용")
         // .author("하하")
         .build();
    
    • 필수 멤버와 선택적 멤버를 분리할 수 있다.

      • 초기화를 반드시 해야하는 변수 혹은 초기화가 선택적인 변수가 있을 수 있다.
      • 이러한 변수들을 가지고 생성자 방식으로 구현하려면 복잡해진다.
        • 초기화가 필수적인 멤버 변수를 위한 생성자 정의 -> 선택적인 멤버 변수를 위한 생성자 오버로딩 -> 혹은 전체 멤버를 파라미터로 받는 생성자를 선언하되, 파라미터에 null을 받도록 구현…
        • 빌더 패턴에서는 빌더의 생성자에 초기화가 필수인 멤버를 넣어주면 된다.
    • 멤버 별로 초기화 검증을 분리할 수 있다.

      • 오버로딩된 생성자가 잔뜩 있다면, 각 생성자 매개변수에 대한 검증 로직을 메서드마다 구현해야 한다.
      • 그러나 빌더의 멤버 설정 메서드에서 검증 과정을 분담하면 이러한 복잡함이 해소된다.
    • 멤버에 대한 변경 가능성 최소화를 추구한다.

      • 멤버에 값을 할당할 때 Setter 메서드를 사용하는 것은 매우 좋지 않다.
      • 개발 시 가장 중요하게 여겨지는 것 중 하나가 불변 객체이다.

        • 불변 객체

          • 객체 생성 후 내부 상태가 변하지 않는 객체
          • 읽기(get) 메서드만 제공
          • 대표적으로 자바에서 final을 붙인 변수가 불변 객체에 해당

          • 불면 객체를 사용해야 하는 이유
            • Thread-Safe하여 동기화를 고려하지 않아도 된다.
            • 가변 객체로 작업을 하면 개발하던 중 예외가 발생 시 해당 객체가 불안정한 상태에 빠질 수 있다. -> 또 다른 오류를 유발시킬 위험이 존재한다.
            • 불변 객체 사용 시 다른 사람이 개발한 함수를 위험 없이 사용할 수 있다. -> 협업과 유지보수에도 유용하다.
      • 클래스가 반드시 가변적이어야 하는 합당한 이유가 있지 않다면, 반드시 불변으로 만들어야 한다. -> 불가능하다면 변경 가능성을 최소화해야 한다.
  • 빌더 패턴의 단점

    • 코드 복잡성 증가

      • 기본적으로는 N개의 클래스에 대해 N개의 빌더 클래스를 만들어야 한다. -> 관리해야 할 클래스가 많아지고 구조가 복잡해진다.
    • 생성자에 비해 성능이 감소한다.

      • 매번 메서드를 호출하고, 빌더를 거쳐 인스턴스화 하기 때문이다.
      • 생성 비용 자체는 크지 않지만, 애플리케이션의 성능을 매우 중요하게 고려해야 하는 상황이라면 문제가 될 수 있다.
    • 지나친 빌더 남용으로 이어질 수 있다.

      • 클래스 필드 개수가 4개보다 적고, 필드의 변경 가능성이 없다면 생성자나 정적 패토리 메서드를 이용하는 것이 좋다.
      • 그러나 API는 시간이 지날수록 매개변수가 많아지는 경향이 있어서, 처음부터 빌더 패턴으로 시작하는 편이 나을 때가 많기도 하다.

질문 Dto 작성

record로 DTO를 작성해보자.

RECORD

  • 값의 집합으로 이루어진 간단한 객체를 개발할 때 사용할 수 있다.

    • 코틀린의 data 클래스에 해당한다.
  • 불변 데이터(immutable data)를 다루는 클래스 구현에 최적화되어 있다.

  • DTO 특성 클래스 개발 시 개발자가 직접 구현하던 반복적인 작업이 불필요해졌다.

    • constructor, accessors(), equals(), hashCode(), toString(), …
  • 데이터 전송 용도라는 것을 명시적으로 나타낼 수 있다. -> 코드에 대한 이해도를 높이고 클래스 목적에 맞지 않는 구현을 방지한다.

  • Record 클래스 특징

    • final 클래스이다.
      • 다른 클래스와의 상속 관계는 생성할 수 없다.
    • 자동으로 생성되는 accessor는 인스턴스 멤버 변수 이름과 동일하다.
    • 접근 제어자는 public, package-private만 가능하다.
    • 생성자의 접근 제어자는 클래스의 접근 제어자보다 제한된 수준일 수 없다.
  • Record 클래스로 DTO 클래스를 만드는 이유

    • 데이터 전송 객체는 단순히 데이터를 전달하기 위해서 사용된다.
      • 다양한 집계 연산을 수행하고 결과를 담는다.
      • 외부 시스템과 통신할 때 불필요한 데이터를 제거한다.
public record QuestionRequest(
  String subject,
  String content
) {

}
@Builder
public record QuestionResponse(
  String subject,
  String content,
  Long authorId,
  String author,
  List<AnswerResponse> answers,
  LocalDateTime modifiedDate
)