공부/Spring Boot

[Spring Boot] 프로그래머스 과제관 「주문관리 API 개발」 jpa lombok 어노테이션없이 스프링 부트 개발하기 가능?

jihyee 2022. 1. 6. 03:29

프로그래머스 과제관에서 스프링 부트 프레임워크 기반으로 api 개발하는 과제가 있어 도전했다!

 

 

(੭•̀ᴗ•̀)੭

 

 

인턴포함 crud 짜는 건 많이 경험했다고 생각했고 주문 관리 정도야 하면서 시작했는데

 

...

 

정말 너무 어려웠고 .. 결국 깃헙의 도움을..,, 받을 수밖에 없었다..

 

(›´ω`‹ )

 

꼭 혼자 하고 싶었는데..

 

 

 

특히 어려웠던 이유를 정리하면

  1. jpa.. jpa 정말 너무 소중하다..
  2. Lombok 너무 소중하다...

 

 

기능이 정말 좋은 라이브러리를 항상 사용하면서 코드 개발을 했구나 하는 생각을 많이 했다.

 

옛날에 김영한님의 스프링 강좌 들을 때 옛날엔 스프링 부트로 코드 짜는 게 정말 어려웠다며 jpa를 굉장히 강조하셨던 게 생각났다..

 

아 이래서 ....

 

개발할 때 경험만큼 좋은 공부는 없다는 것을 또 한 번 느꼈고 강의를 여러 번 들어도 이해가 되지 않았던 jdbc와 jpa의 차이를 보다 선명하게 이해할 수 있었다.

 

 

 

 

 

어려웠던 이유와 이해한 내용을 코드와 정리해보면,

 

 

 

jpa vs jdbc
@Repository
public class JdbcOrderRepository implements OrderRepository{

  private final JdbcTemplate jdbcTemplate;

  public JdbcOrderRepository(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  @Override
  public List<Order> findAll(Pageable pageable) {

    String query = "SELECT * FROM orders ORDER BY seq DESC "
    + " LIMIT " + pageable.getSize() + " OFFSET " + pageable.getOffset();

    return jdbcTemplate.query(
      query,
      mapper
    );
  }
  static RowMapper<Order> mapper = (rs, rowNum) ->
    new Order.Builder()
      .seq(rs.getLong("seq"))
      .userId(rs.getLong("user_seq"))
      .productId(rs.getLong("product_seq"))
      .reviewId(rs.getLong("review_seq"))
      .state(State.valueOf(rs.getString("state")))
      .requestMessage(rs.getString("request_msg"))
      .rejectMessage(rs.getString("reject_msg"))
      .completedAt(dateTimeOf(rs.getTimestamp("completed_at")))
      .rejectedAt(dateTimeOf(rs.getTimestamp("rejected_at")))
      .createAt(dateTimeOf(rs.getTimestamp("create_at")))
      .build();
}

 

 

이게 jdbcTemplate을 사용한 repository인데 jpa를 항상 사용했던 입장에선 굉장히 생소한 코드였다..

(생소한 정도가 아니라 사실 처음 봤다..)

 

 

jdbcTemplate에 string 형식 그대로의 쿼리문

String query = "SELECT * FROM orders ORDER BY seq DESC "
    + " LIMIT " + pageable.getSize() + " OFFSET " + pageable.getOffset();

을 보내면 데이터베이스의 출력 값을 받아오고 그걸 mapper를 통해 하나하나 Order 객체로 매핑시키고 있다.

 

 

칼럼 수만큼 매핑해줘야 한다..

 

처음에 혼자 과제를 하면서 막혔던 부분이 바로 이 부분이다.

 

프로그래머스에서 하는 과제이다 보니 h2 데이터베이스에 값이 이미 들어간 채로 했는데 매핑을 하려면 디비 테이블의 칼럼명을 알아야했다물론 디비에는 소문자 _ 형식으로 컬럼명을 쓰자는 규칙이 있지만 pk 칼럼들이 당연히 id 되어있는  알고 seq 들을  id  적었더니 해당 칼럼명이랑 매핑되는  없다고 계속 에러가 났다정신이 아득..  많은 칼럼명들을  하나하나 해봐야 하는 건가..?? 게다가 자동 entity 연관 관계 매핑을 하는 jpa 써왔다 보니 reivew  자체가  다른 entity 때문에  다른 칼럼들처럼 getLong 이런 식으로 받아오면  된다고 생각했다찾아보니 getObject 있어서  이거구나 하고 바로 썼는데..

 

어림도 없는..,, (›´ω`‹ )

 

하.. 진짜 포기하고 싶었다..

 

 

 

코드를 jpa 사용한다고 가정하고 바꾸면

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
}

 

 

장난 같겠지만 정말 이게 끝이다. ᵔ◡ᵔ

 

 

jpa 쓰면 findAll과 같은 메서드는 내장 메서드로 기능이 박혀있어서 JpaRepository만 상속받아주면 그 자체로 그냥 끝이다.. 

 

jdbc 코드와 비교해보면 자바 스프링으로 개발할 때 jpa가 왜 필요한지 왜 써야 하는지 이해할 수밖에 없다. 단순하게 코드 길이만 비교해도 34줄 → 3줄..

 

아마 JpaRepository findAll 내장 메서드 다 뜯어보면 위에서 jdbcTemplate query문 포함해서 Oder 엔티티에 매핑하는 코드가 들어있을 것이다. jpa 사용을 안 할 이유는 없지만 내장 메서드만으로는 해결이 안 되는 경우도 있고 결국 저 jdbc 코드를 기반으로 jpa가 있는 거니까 원 코드를 알아서 나쁠 건 없다고 본다.

 

 

 

lombok 어노테이션

또 하나 정신을 아득하게 한 게 바로 롬복이다.

 

개발할 때 롬복은 그냥 무조건 깔고 가라고 해서 초기 프로젝트 설정할 때 당연하게 하고 넘어갔어서 이렇게 소중한 존재인 줄 몰랐다...

 

사실 자바 공부를 하려면 롬복을 쓰고 직접 getter setter constructor builder toString 구현해보는 좋을 같긴 한데 코드 보면 알겠지만 너무 반복적인 코드가 많긴 하다.

 

public class Order {

  private final Long seq;

  private Long userId;

  private Long productId;

  private Long reviewId;

  private Review review;

  private State state;

  private String requestMessage;

  private String rejectMessage;

  private LocalDateTime completedAt;

  private LocalDateTime rejectedAt;

  private final LocalDateTime createAt;

    public Order(Long seq,
    Long userId,
    Long productId,
    Long reviewId,
    State state,
    String requestMessage,
    String rejectMessage,
    LocalDateTime completedAt,
    LocalDateTime rejectedAt,
    LocalDateTime createAt){
    
    this.seq = seq;
    this.userId = userId;
    this.productId = productId;
    this.reviewId = reviewId;
    this.state = state;
    this.requestMessage = requestMessage;
    this.rejectMessage = rejectMessage;
    this.completedAt = completedAt;
    this.rejectedAt = rejectedAt;
    this.createAt = createAt;

  }

  public Long getSeq() {
    return seq;
  }

  public Long getUserId() {
    return userId;
  }

  public void setUserId(Long userId) {
    this.userId = userId;
  }

  public Long getProductId() {
    return productId;
  }

  public void setProductId(Long productId) {
    this.productId = productId;
  }

  public Review getReview() {
    return review;
  }

  public void setReview(Review review) {
    this.review = review;
  }

  public Long getReviewId() {
    return reviewId;
  }

  public void setReviewId(Long reviewId) {
    this.reviewId = reviewId;
  }
  
  public State getState() {
    return state;
  }

  public void setState(State state) {
    this.state = state;
  }
  
  public String getRequestMessage() {
    return requestMessage;
  }

  public void setRequestMessage(String requestMessage) {
    this.requestMessage = requestMessage;
  }
  
  public String getRejectMessage() {
    return rejectMessage;
  }

  public void setRejectMessage(String rejectMessage) {
    this.rejectMessage = rejectMessage;
  }

  public LocalDateTime getCompletedAt() {
    return completedAt;
  }

  public void setCompletedAt(LocalDateTime completedAt) {
    this.completedAt = completedAt;
  }

  public LocalDateTime getRejectedAt() {
    return rejectedAt;
  }

  public void getRejectedAt(LocalDateTime rejectedAt) {
    this.rejectedAt = rejectedAt;
  }

  public LocalDateTime getCreateAt() {
    return createAt;
  }

  @Override
  public String toString() {
    return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
      .append("seq", seq)
      .append("userId", userId)
      .append("productId", productId)
      .append("reviewId", reviewId)
      .append("review", review.toString())
      .append("state", state)
      .append("requestMessage", requestMessage)
      .append("rejectMessage", rejectMessage)
      .append("completedAt", completedAt)
      .append("rejectedAt", rejectedAt)
      .append("createAt", createAt)
      .toString();
  }

  static public class Builder {
    
    private Long seq;
    private Long userId;
    private Long productId;
    private Long reviewId;
    private State state;
    private String requestMessage;
    private String rejectMessage;
    private LocalDateTime completedAt;
    private LocalDateTime rejectedAt;
    private LocalDateTime createAt;

    public Builder() {/*empty*/}

    public Builder(Order order) {
      this.seq = order.seq;
      this.userId = order.userId;
      this.productId = order.productId;
      this.reviewId = order.reviewId;
      this.state = order.state;
      this.requestMessage = order.requestMessage;
      this.rejectMessage = order.rejectMessage;
      this.completedAt = order.completedAt;
      this.rejectedAt = order.rejectedAt;
      this.createAt = order.createAt;
    }

    public Builder seq(Long seq) {
      this.seq = seq;
      return this;
    }
    public Builder userId(Long userId) {
      this.userId = userId;
      return this;
    }
    public Builder productId(Long productId) {
      this.productId = productId;
      return this;
    }
    public Builder reviewId(Long reviewId) {
      this.reviewId = reviewId;
      return this;
    }
    public Builder state(State state) {
      this.state = state;
      return this;
    }
    public Builder requestMessage(String requestMessage) {
      this.requestMessage = requestMessage;
      return this;
    }
    public Builder rejectMessage(String rejectMessage) {
      this.rejectMessage = rejectMessage;
      return this;
    }
    public Builder completedAt(LocalDateTime completedAt) {
      this.completedAt = completedAt;
      return this;
    }
    public Builder rejectedAt(LocalDateTime rejectedAt) {
      this.rejectedAt = rejectedAt;
      return this;
    }
    public Builder createAt(LocalDateTime createAt) {
      this.createAt = createAt;
      return this;
    }


    public Order build() {
      return new Order(
        seq,
        userId,
        productId,
        reviewId,
        state,
        requestMessage,
        rejectMessage,
        completedAt,
        rejectedAt,
        createAt
      );
    }
  }
}

...

 

보면 class 위에 어떤 어노테이션도 없는 걸 확인할 수 있는데 이게 롬복 어노테이션들을 하나도 안 쓴 순수한 데이터 클래스이다.

 

롬복 어노테이이션이랑 entity 어노테이션 사용한 코드로 바꾸면

 

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "JOINS")
public class Join {
    @JsonIgnore
    @Id
    @Column(name = "JOIN_SEQ")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long joinSeq;

    @NotNull
    @ManyToOne(targetEntity = User.class, fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
    @JoinColumn(name = "USER_SEQ", referencedColumnName = "user_seq")
    private User joinUser;

    @NotNull
    @ManyToOne(targetEntity = Study.class, fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
    @JoinColumn(name = "STUDY_SEQ", referencedColumnName = "study_seq")
    private Study joinStudy;

    @NotNull
    @Enumerated(EnumType.STRING)
    private JoinType joinType;

    @Column(name = "COMMENT")
    private String comment;
}

코드가 이렇게 간결해진다.

 

이 정도면 안 쓸 이유가 없겠다.

롬복 어노테이션들의 기능을 하나씩 보면

 

@Getter

get형 메서드 자동 생성
@Setter

set형 메소드 자동 생성
@NoArgsConstructor

파라미터 없는 생성자 자동 생성
@AllArgsConstructor

파라미터 채운 생성자 자동 생성

@toString

toString 메소드 자동 생성

@builder

builder 클래스 자동 생성

 

 

6가지 롬복 어노테이션으로 entity 나 dto 등의 데이터 정의 부분에서 코드를 간결하게 줄일 수 있고 반복적인 개발 과정을 수월하게 도와준다.

 

lombok 최고 ( ・ᴗ・̥̥̥ )

 

 

 

과제와 함께 jdbc와 jpa의 차이, 롬복 어노테이션에 대해 공부할 수 있어서 좋았던 것 같고 이런 과제가 아니었더라면 존재조차 모르고 넘어갔을 것 같아서 더 중요했던 것 같다.

 

jpa는 편한 만큼 어렵고 공부가 많이 필요한 부분이라는 것에 이유도 많이 느꼈고 라이브러리나 예시 코드 없이 코드를 통으로 혼자 짜는 것도 많이 연습해야겠다고 생각했다.

 

 

 

 

 

(*•̀ᴗ•́*)و ̑̑