프로그래머스 과제관에서 스프링 부트 프레임워크 기반으로 api 개발하는 과제가 있어 도전했다!
(੭•̀ᴗ•̀)੭
인턴포함 crud 짜는 건 많이 경험했다고 생각했고 주문 관리 정도야 하면서 시작했는데
...
정말 너무 어려웠고 .. 결국 깃헙의 도움을..,, 받을 수밖에 없었다..
(›´ω`‹ )
꼭 혼자 하고 싶었는데..
특히 어려웠던 이유를 정리하면
- jpa.. jpa 정말 너무 소중하다..
- 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는 편한 만큼 어렵고 공부가 많이 필요한 부분이라는 것에 이유도 많이 느꼈고 라이브러리나 예시 코드 없이 코드를 통으로 혼자 짜는 것도 많이 연습해야겠다고 생각했다.
(*•̀ᴗ•́*)و ̑̑
'공부 > Spring Boot' 카테고리의 다른 글
[Spring Boot] Service 계층과 DTO 존재 이유에 대한 생각 (0) | 2021.12.18 |
---|---|
[AWS + Spring Boot ] 스프링부트 QueryDSL 연동 배포 (0) | 2021.10.31 |