Intro


최적의 리소스를 찾는 능력

Spring Framework를 배우는 짧은 시간동안 방대한 교육 내용을 진행하며 모든 것을 단번에 이해하기는 어려웠다. 출퇴근 시간이나, 수업이 끝난 후의 쉬는시간, 코어 타임 이외의 시간들을 활용하여 교육자료 뿐만이 아닌 인터넷 상에 존재하는 무수히 많은 래퍼런스나 문서들을 디깅하게 된다.

기본적인 개념을 숙지하기 위한 지식은 프레임워크나 라이브러리 또는 해당 언어가 지원하는 공식 사이트에서 충분히 찾아볼 수 있었다. 그런데 해당 지식들을 어떻게 활용하는지, 트러블 슈팅은 어떻게 했는지에 대해서는 번역 등의 문제로 인해 100에 70은 국내 문서를 살펴보게 되는 것 같다.

물론 국내 문서가 외국 문서보다 신뢰성이 떨어진다는 뜻은 아니지만, 국내 문서들의 대부분은 내부 콘텐츠의 질이 거의 똑같다. 그래서 똑같은 정보를 가진 여러 게시글을 자주 접하게 된다. 그래서 내가 원하는 내용이 포함된 게시글을 찾으려면 꽤나 시간을 들여야 했다. 여기서 느꼈던 점은 리소스를 찾는 능력이 정말로 중요하다는 것이다.

리소소를 찾는 능력이라는 것은 결국 시간과 비용에 관련이 되는데 나같은 비기너일수록 당장 필요한 리소스를 찾는데 드는 시간과 비용이 주니어 및 시니어분들에 비해 높게 책정될 것이다. 그래서 당장 궁금하거나 필요한 리소스를 찾을 때 아무 생각없이 구글 검색창에 닥치는대로 검색하는 것보단 먼저 신중하게 검색 키워드를 생각해내는 것이 앞에서 말한 시간과 비용을 절약할 수 있을 것이라고 생각이 들었다.

이번 주간에는 이러한 리소스를 찾는 능력의 중요성을 새삼 느끼게 되었고, 이 능력을 기르기 위해서 개발하기 전 설계하는 것처럼 검색하기 전 내가 무엇을 찾고자 하는지 스스로 정리해볼 수 있는 시간을 좀 더 두려고 한다.




Day - 54

@ModelAttribute 어노테이션의 동작 원리

thymeleaf 템플릿 엔진을 통해 화면 레이아웃을 구성하는 코드를 살펴보던 중 뷰에게 전달하는 model의 값이 없는데도 불구하고 뷰에서 model의 데이터를 받아서 출력하고 있음을 확인하였다.

문제의 코드는 다음과 같다.

@GetMapping({"/board/read", "/board/modify"})
public void read(@ModelAttribute("requestDTO") PageRequestDTO pageRequestDTO, Long bno, Model model) {
    log.info("[BoardController] /board/read bno: " + bno);
    BoardDTO dto = boardService.get(bno);
    model.addAttribute("dto", dto);
    // requestDTO를 model에 담아서 전달하고 있지 않다..?
}

그런데 view(화면)에는 아래와 같이 requestDTO를 받아서 정상적으로 출력하고 있음을 확인할 수 있었다.

<!-- 어떻게 requestDTO의 값을 알고 있는 걸까..? -->
<a th:href="@{/board/modify(bno = ${dto.bno}, page=${requestDTO.page}, type=${requestDTO.type}, keyword =${requestDTO.keyword})}">
  <button type="button" class="btn btn-primary">수정</button>
</a>
<a th:href="@{/board/list(page=${requestDTO.page} , type=${requestDTO.type}, keyword =${requestDTO.keyword})}">
  <button type="button" class="btn btn-info">목록</button>
</a>

model로 넘겨주지 않았는데 어떻게 뷰에서 requestDTO를 알고 있는지 궁금해서 컨트롤러에 작성된 @ModelAttribute 어노테이션을 파헤쳐 보기로 했다.

@ModelAttribute 어노테이션은 HTTP Body 내용과 HTTP 파라미터의 값들을 Getter, Setter, 생성자를 통해 주입하기 위해 사용한다. 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 하는데 @ModelAttribute 어노테이션은 이 절차를 자동화해주는 기능을 제공한다.

그래서 별도로 model 객체를 통해서 전달하지 않고도 view에서 사용할 수 있었던 것이었다. 그렇다면 어떤 원리로 자동화하여 view에게 전달할 수 있었던 것인지 알아보자.


@ModelAttribute 어노테이션의 동작 흐름

  1. 먼저 HTTP 요청 파라미터로 전달받은 타입의 객체를 생성한다. 이 때, 생성되는 객체는 getter와 setter가 존재하여야 정상적으로 전달받은 파라미터 값을 주입받을 수 있다.

  2. @ModelAttribute 어노테이션을 통해서 HTTP 요청 파라미터를 통해서 전달받은 값들을 지정된 객체에 바인딩한다.

    http://localhost:8080/member?mid=10&nickname=lango
    위와 같은 요청에 대해서는 member라는 객체에 대해서 mid는 10, nickname은 lango 라는 값을 setter를 통해서 해당 멤버 변수에게 바인딩하게 된다.

  3. 마지막으로 @ModelAttribute 어노테이션이 붙은 객체는 자동으로 model 객체에 추가되고 view에게 전달된다.

    @ModelAttribute("member") Member member 라는 구문을 보면 어노테이션 괄호안의 member라는 이름으로 view 단에서 전달받은 member 값을 호출하여 사용할 수 있게 된다.


@ModelAttribute 어노테이션 실습해보기

간단하게 @ModelAttribute 어노테이션을 활용해 자동으로 view에서 데이터를 받아보는 에제를 작성해보자.

MemberDTO.java

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class MemberDTO {
    private String email;
    private String nickname;
}

member.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h2 th:text="${member.email}"></h2>
    <h2 th:text="${member.nickname}"></h2>
</body>
</html>

Member 객체의 데이터 교환을 담당할 MemberDTO 객체를 만들고, view에서 member라는 데이터를 잘 받아오는지 확인해보기 위해 간단하게 h2 태그로 member의 email과 nickname 값을 출력해볼 HTML 파일도 하나 만들었다.

Controller.java

@GetMapping("/member")
public String read(@ModelAttribute("member") MemberDTO dto) {
	log.info("MemberDTO: " + dto);
	return "member";
}

그리고 컨트롤러에서 요청 파라미터로 전달받은 값을 @ModelAttribute("member") MemberDTO dto) 구문을 통해서 @ModelAttribute 어노테이션을 통해 MemberDTO에 바인딩하여 view로 보내도록 설정하였다.

자 서버쪽 준비는 완료되었으니, 브라우저에서 요청을 보내보자.

http://localhost/member?email=lango@kakao.com&nickname=lango

로그 출력을 확인해보니 요청 파라미터 email과 nickname의 값을 MemberDTO 객체로 바인딩하여 dto라는 지역변수에 잘 담은 것을 확인할 수 있다.

MemberDTO: MemberDTO(email=lango@kakao.com, nickname=lango)

마지막으로 member.html 파일에서 출력했던 member의 email과 nickname이 잘 출력될까?

의도했던 대로 member의 email과 nickname 값을 view에서 받아서 사용할 수 있음을 확인하였다.

결국 @RequestParam 어노테이션으로 파라미터를 받아와서 model 객체에 담는 과정들을@ModelAttribute 어노테이션을 통해서 간소화 및 자동화할 수 있다고 느꼈다.



Day - 55

금일에는 영화 리뷰 등록 실습 프로젝트를 진행하면서 조금 생소한 애노테이션들을 사용하게 되었다. 이용한 어노테이션들의 용도는 무엇이고 어떤 용도로 쓰이는지 공부하는 과정이 생각보다 재미있었다.


@EntityGraph를 통한 Fetch Join 기능

특정 Entity의 값을 조회하여 가져올 때 지연로딩을 적용해놓은 속성이 연관 관계가 있는 다른 Entity일 경우에 @EntityGraph 어노테이션을 사용했었다. 그래서 @EntityGraph 어노테이션은 어떤 기능을 하는 것인지 알아보려 한다.

@EntityGraph 어노테이션은 엔티티를 조회할 때 연관 관계가 맺어진 속성에 지연 로딩이 적용되어 있을 경우 종속된 엔티티를 함께 조회(Select)하지 않고, Proxy 객체를 만들어서 적용시키는 기능을 제공한다. 이 후에는 해당 Proxy 객체를 호출할 때마다 조회(Select) 쿼리를 실행된다.

말 그대로 하나의 Entity에 속한 다른 Entity를 Fetch Join하여 함께 가져오게 된다.

Fetch Join이란 주체가 되는 Entity 이외에 Fetch Join이 적용된 연관 Entity도 함께 SELECT 하여 모두 영속화하는 작업이다.

정리하자면 @EntityGraph는 Spring Data JPA에서 FETCH JOIN을 간소화된 버전으로 사용할 수 있는 어노테이션이라고 생각하면 된다.


Movie(영화)의 PK로 Review(리뷰)를 가져오는 예제

회원(Member)과 영화(Movie), 그리고 리뷰(Review)라는 객체를 만든다.
회원과 영화는 독립적인 객체이며 한 명의 회원은 하나의 영화에 대해서 여러 리뷰를 작성할 수 있다. 예제에서는 특정 Movie(영화) 번호로 리뷰글을 가져오는데 리뷰의 작성자도 함께 가져오는 작업을 해보려고 한다.

먼저 Movie, Member, Review 3개의 Entity를 만들고 Review가 N개의 Membe와 Movie를 가질 수 있도록 관계를 설정하자.

Member.java

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
@Table(name = "m_member")
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mid;
    private String email;
    private String password;
    private String nickname;
}

Movie.java

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
@Entity
public class Movie extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mno;

    private String title;
}

Review.java

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
// 지연로딩을 위해 toString시 movie와 member는 제외하고 호출하도록 설정한다.
@ToString(exclude = {"movie", "member"})
@Entity
public class Review {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long reviewnum;
    @ManyToOne(fetch = FetchType.LAZY)
    private Movie movie;
    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;
    private int grade;
    private String text;
}

위 코드를 보면 Review Entity에 @ManyToOne 어노테이션을 통해 Movie와 Member의 외래 키를 설정하는데 지연로딩을 적용하였다.

ReviewRepository.java

// 영화 정보를 찾아오는 메소드
    @EntityGraph(attributePaths = {"member"}, type = EntityGraph.EntityGraphType.FETCH)
    List<Review> findByMovie(Movie movie);

그리고 ReviewRepository에서 findByMovie라는 메소드를 정의한다. 이 때 @EntityGraph 어노테이션을 통해 Member 객체를 함께 Fetch 해오도록 설정한다.


테스트 진행

@Test
public void getReviews() {
	Movie movie = Movie.builder().mno(2L).build();
	List<Review> result = reviewRepository.findByMovie(movie);
	result.forEach(review -> {
		System.out.println(review.getReviewnum());
		System.out.println(review.getMember().getEmail());
	});
}

이제 테스트클래스에서 findByMovie 메소드를 통해 2번 영화의 리뷰 번호와 리뷰 작성자를 확인하는 테스트를 진행하여 확인해보자.

Hibernate: 
    select
        r1_0.reviewnum,
        r1_0.grade,
        m1_0.mid,
        m1_0.email,
        m1_0.nickname,
        m1_0.password,
        r1_0.movie_mno,
        r1_0.text 
    from
        review r1_0 
    left join
        m_member m1_0 
            on m1_0.mid=r1_0.member_mid 
    where
        r1_0.movie_mno=?
50
kakao-cloud-42@kaka.com
84
kakao-cloud-25@kaka.com
120
kakao-cloud-10@kaka.com

다음과 같이 join 쿼리를 통해 한번만 조회 작업을 하게 된다. 그래서 2번 영화에 대한 리뷰글은 3개가 작성되었음을 확인할 수 있었다. kakao-cloud-42@kaka.com라는 이메일을 가진 작성자가 50번의 리뷰글을 작성했고 kakao-cloud-25@kaka.com라는 이메일을 가진 작성자가 84번의 리뷰글을 작성했고, kakao-cloud-10@kaka.com라는 이메일을 가진 작성자가 120번의 리뷰글을 작성하였다.


@EntityGraph 어노테이션 없이 사용한다면?

만약 findByMovie 메소드에 @EntityGraph 어노테이션을 붙이지 않는다면 어떻게 될까?

List<Review> findByMovie(Movie movie);

findByMovie메소드에 @EntityGraph 어노테이션을 제거하였다.

@Test
@Transactional
public void getReviews() {
	Movie movie = Movie.builder().mno(2L).build();
	List<Review> result = reviewRepository.findByMovie(movie);
	result.forEach(review -> {
		System.out.println(review.getReviewnum());
		System.out.println(review.getMember().getEmail());
	});
}

그리고 테스트 메소드에서 @Transactional 어노테이션을 붙여서 테스트를 해보자.

테스트 메소드에서 @Transactional 어노테이션을 붙인 이유는 Review에서 Member나 Movie를 가져오려고 하면 지연로딩이 적용되어 있기 때문에 메소드의 종료시점까지 DB 연결을 지속시키기 위함이다.

Hibernate: 
    select
        r1_0.reviewnum,
        r1_0.grade,
        r1_0.member_mid,
        r1_0.movie_mno,
        r1_0.text 
    from
        review r1_0 
    where
        r1_0.movie_mno=?
50
Hibernate: 
    select
        m1_0.mid,
        m1_0.email,
        m1_0.nickname,
        m1_0.password 
    from
        m_member m1_0 
    where
        m1_0.mid=?
kakao-cloud-42@kaka.com
84
Hibernate: 
    select
        m1_0.mid,
        m1_0.email,
        m1_0.nickname,
        m1_0.password 
    from
        m_member m1_0 
    where
        m1_0.mid=?
kakao-cloud-25@kaka.com
120
Hibernate: 
    select
        m1_0.mid,
        m1_0.email,
        m1_0.nickname,
        m1_0.password 
    from
        m_member m1_0 
    where
        m1_0.mid=?
kakao-cloud-10@kaka.com

콘솔에서 실행된 쿼리를 보면 @EntityGraph 어노테이션을 적용했을 때와는 달리 Reivew Entity에서 review.getMember().getEmail(); 구문을 통해 Member에 접근할 때 Reivew의 데이터가 3개이기에 3번의 Select 쿼리를 실행하는 것을 확인할 수 있었다.


@EntityGraph 어노테이션은 무조건 사용해야 할까?

위에서 본 것처럼 @EntityGraph 어노테이션을 통해서 Join 쿼리를 실행하여 한번에 데이터를 가져오는 작업을 해보았다. 물론 Join을 통해 한번만 DB에 접근하면 되기에 소규모의 프로젝트에서는 괜찮을 것 같지만 대규모 프로젝트나 서비스가 될 수록 Join이 많아진다는 것은 여간 부담이 아닐 수 없을 것이라고 생각한다.

그래서 지연로딩을 통해 필요한 조회 쿼리를 그때그때 실행하는 때와 @EntityGraph 어노테이션을 통해 Join을 실행해야 할 상황을 잘 구분지어 구조화하는 능력을 길러야 한다고 느꼈다.



수정에 사용되는 @Modifying 어노테이션에 대해서

실습 프로젝트를 진행하는데 아래와 같이 회원을 수정하는 메소드를 작성할 때 @Modifying 어노테이션을 사용하는데 왜 @Modifying 어노테이션을 사용하는지 살펴보려 한다.

@Modifying 
@Query("update Review r " +
        "set r.member.mid = null " +
        "where r.member.mid = :mid")
void updateByMember(@Param("mid") Long mid);

위 코드의 updateByMember 메소드에 붙인 @Modifying 어노테이션은 @Query 어노테이션(JPQL Query, Native Query)을 통해 작성된 SELECT 제외한 INSERT, UPDATE, DELETE 쿼리에서 사용되는 어노테이션이라고 한다.

@Modifying 어노테이션에 대한 정보를 공식 문서 번역본에서 찾아보았다. 앞서 살펴본 대로 @Query 어노테이션과 함께 사용해야 한다고 한다.

@Modifying 어노테이션의 특징은?

@Modifying 어노테이션은 다음과 같은 특징을 가진다.

  • 기본적으로 JpaRepository에서 제공하는 메서드 혹은 메서드 네이밍으로 만들어진 쿼리에는 적용되지 않는다.
  • clearAutomatically, flushAutomatically 속성을 변경할 수 있으며 주로 벌크(Bulk) 연산과 같이 이용된다.
  • JPA Entity Life-cycle을 무시하고 쿼리가 실행되기 때문에 해당 어노테이션을 사용할 때는 영속성 컨텍스트 관리에 주의해야 한다. 이는 앞에서 본 clearAutomatically, flushAutomatically 속성을 이용하여 간단하게 해결할 수 있다.

벌크(Bulk) 연산이란 단 한번의 UPDATE, DELETE 연산을 제외한 여러 번의 UPDATE, DELETE 연산을 하나의 쿼리로 작업하는 것을 의미한다. JPA에서 한 건의 UPDATE 같은 경우에는 Dirty Checking(변경 감지)를 통해서 수행되거나 save()를 통해 수행할 수 있다. DELETE 경우 한건이나 여러 건을 쿼리 메서드로 제공된다.


회원 정보를 수정하는 예제

그렇다면 이번 글의 처음에서 봤던 코드처럼 회원을 수정하는 예제를 통해 @Modifying 어노테이션의 사용 유무를 확인해보자.

먼저 회원 Entity는 다음과 같다.

Member.java

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
@Entity
public class Member extends BaseEntity {
    @Id
    private String email;
    private String password;
    private String name;
}

간단하게 email 문자열을 기본 키로 가지는 회원 객체이다. 다음으로 MemberRepository 인터페이스에 회원의 이름을 수정하는 추상 메소드를 작성하였다.

MemberRepository.java

public interface MemberRepository extends JpaRepository<Member, String> {
    @Query("update Member m " +
            "set m.name = :name " +
            "where m.email = :email")
    void updateByName(@Param("name") String name, @Param("email") String email);
}

이 때는 그냥 update 쿼리이지만 @Modifying 어노테이션 없이 @Query 어노테이션만을 작성하였다.이제 마지막으로 updateByName 메소드를 테스트할 차례이다.

MemberRepositoryTest.java

@Test
@Transactional
public void updateMemberName() {
	Member member = Member.builder()
    		.email("user100@kakao.com")
        	.name("lango")
        	.build();

	System.out.println(member.getName() + " " + member.getEmail());
    memberRepository.updateByName(member.getName(), member.getEmail());

	Optional<Member> lango = memberRepository.findById("user100@kakao.com");
	if (lango.isPresent()) {
    	System.out.println(lango);
    }
}
Caused by: org.hibernate.query.IllegalSelectQueryException: Expecting a SELECT Query [org.hibernate.query.sqm.tree.select.SqmSelectStatement], but found org.hibernate.query.sqm.tree.update.SqmUpdateStatement [update Member m set m.name = :name where m.email = :email]

딱히 문제될 것이 없다고 생각했지만 위와 같은 에러가 발생하였다.

다시 한번 @Modifying 어노테이션을 공식문서를 통해 확인해보니 다음과 같이 @Query 어노테이션 선언을 통해 DML(INSERT, UPDATE, DELETE)문을 실행할 경우 반드시 @Modifying을 붙이라는 내용을 확인할 수 있었다.

Queries that require a `@Modifying` annotation include INSERT, UPDATE, DELETE, and DDL statements.

예제 코드 수정

그래서 MemberRepository의 updateByName 메소드에 @Modifying 어노테이션을 붙여보았다.

MemberRepository.java

@Modifying
@Query("update Member m " +
		"set m.name = :name " +
        "where m.email = :email")
void updateByName(@Param("name") String name, @Param("email") String email);

또한, 테스트 메소드에도 @Commit 어노테이션을 붙여 트랜잭션을 커밋하도록 하여 DB 값도 정상적으로 변경되는지를 확인하려 한다.

MemberRespositoryTest.java

@Test
@Transactional
@Commit
public void updateMemberName() {
	Member member = Member.builder()
    		.email("user100@kakao.com")
            .name("lango")
            .build();

	System.out.println(member.getName() + " " + member.getEmail());
    memberRepository.updateByName(member.getName(), member.getEmail());

	Optional<Member> lango = memberRepository.findById("user100@kakao.com");
    if (lango.isPresent()) {
    	System.out.println(lango);
    }
}

이렇게 두개의 코드를 고치고 updateMemberName 테스트메소드를 실행해보았다. @Modifying 어노테이션을 붙이니 정상적으로 테스트가 통과되어 수정한 Member 객체가 출력됨을 알 수 있었다.

Optional[Member(email=user100@kakao.com, password=kakao100, name=lango)]

DB에서 user100@kakao.com의 name도 lango로 변경된 것을 알 수 있었다.

결국 JPQL을 이용할 때 수정이나 삭제, 삽입 작업을 하려면 @Modifying 어노테이션을 꼭 함께 사용해야 함을 어느정도 이해할 수 있었다.



Day - 56

사용자 정의 쿼리란 무엇인가?

JPA를 사용하면서 JpaRepository가 기본적으로 제공하는 여러 CRUD 메소드가 존재하지만, 까다로운 조회 쿼리와 같은 JpaRepository에서 제공해주지 않는 기능을 구현해야 할 경우라면 어떻게 해야 할까? 예를 들어 사용자 테이블에서 PK가 아닌 이름이나 이메일을 조회하거나 사용자의 나이를 조건으로 조회해야 할 경우 등이 있을 수 있다. 이 경우 사용자 정의 쿼리를 사용해야 한다.

사용자 정의 쿼리란?

사용자 정의 쿼리는 말 그대로 JPA가 자동으로 생성하는 쿼리를 사용하는게 아닌 개발자가 직접 작성한 Query나 Native Query를 말한다.

Native Query는 '순수 쿼리'라는 뜻으로, 말그대로 흔히 사용하는 SQL 쿼리문으로 볼 수 있다. Native Query는 Entity 속성을 사용하지 못하기 떄문에 특정 데이터베이스에 종속적인 Query이다.

이러한 사용자 정의 쿼리를 사용하는 방법에는 Named QueryQuery Method, @Query Anootation을 이용하는 방법이 있다. 여기서는 이 중에 Query Method@Query Annotation 두 가지만 살펴보고자 한다.

네임드 쿼리(Named Query)는 쿼리에 이름을 부여하는 방식이다. 컴파일시 타입체크, 가독성과 같은 부분에서 문제가 조금 있기 때문에 @Query 어노테이션을 사용한다면 지금 당장은 네임드 쿼리의 필요성이 크지는 않을 것 같다.


쿼리 메서드(Query Method)

쿼리 메서드는 메서드 이름을 지정하면 우리가 원하는 기능을 수행할 쿼리가 자동으로 생성해준다. 단순히 Spring Data JPA에서 지정한 네이밍 컨벤션을 지킨다면 JPA가 메서드 이름을 분석하여 알맞는 JPQL을 구성해준다.

위와 같이 간단하게 회원과 관련된 MemberRepository 인터페이스가 있다고 하면 find만 입력해도 자동으로 snippet을 통해 만들 수 있는 쿼리 메서드들을 확인할 수 있다.

대표적으로 And, Or, Is, Equals, Between, LessThen, After, Before, IsNull, OrderBy, Not와 같은 키워드를 붙여 쿼리 메서드를 만들 수 있다.

어떤 키워드로 네이밍을 할 수 있는지는 더 자세하게 알려면 Query Method 공식 문서에서 확인하면 된다.

Query Method의 한계

하지만 쿼리 메서드를 통해서도 모든 조회기능을 구현할 수는 없다. 특정 상황에서는 Native Query를 사용해야 할 수도 있고 여러 조합으로 Query를 짜야하는 상황이 올 수 있다. 이 때는 @Query 어노테이션을 이용하면 개발자가 원하는 쿼리를 직접 짜는데 강력한 기능을 제공한다.


@Query 어노테이션에 사용되는 JPQL?!

세밀한 조회 작업을 해야 할 경우 그에 대응하는 쿼리를 작성할 수 있어야 한다. 이 때 @Query 어노테이션을 실행할 메서드 위에 정적 쿼리를 작성할 수 있는데 해당 쿼리는 JPQL 쿼리로 작성되어야 한다.

JPQL(Java Persistence Query Language)이란 JPA가 제공하는 객체지향 쿼리로 SQL을 추상화한 객체 중심 SQL을 제공한다. JPQL은 기존의 SQL 개발에 익숙하다면 편하고 단순한 방법으로 느껴질 수도 있다. SQL이 테이블을 대상으로 하는 쿼리라고 한다면 JPQL은 엔티티 객체를 대상으로 하는 쿼리라고 할 수 있다.

select m
from Member m
where m.mid = :mid

JPQL을 작성하는 방식은 간단하다. 쿼리문 내부에 다음과 같이 참조변수.필드 와 같은 형태로 작성하면 된다.


@Query 어노테이션 사용법

@Query 어노테이션의 경우 JpaRepository를 상속받은 인터페이스에서 사용할 수 있다. name을 조건으로 Member(회원)의 정보를 조회하는 메소드를 만든다고 하면 @Query 어노테이션에 작성할 JPQL 쿼리는 아래와 같을 것이다.

select m 
from Member m 
where m.name = :name

name을 파라미터로 받아서 회원의 정보를 가져와서 반환하는 findMemberByName 메소드를 작성해보았다.

find를 이용하여 쿼리 메소드의 이름은 일반적으로 find+(엔티티 이름)+By+변수 이름 형식으로 만든다.

public interface MemberRepository extends JpaRepository<Member, Long> {
  	@Query("select m " +
            "from Member m " +
            "where m.name = :name")
    Member findMemberByName(@Param("name") String name);
}

JPQL 쿼리를 잘 보면 알겠지만 DB 테이블명으로 작성하지 않고 직접 만든 Member 객체를 대상으로 작성한 것을 볼 수 있다. 여기서 중요한 점은 DB의 테이블에 쿼리를 전송하는 것이 아니라 스프링에서 만든 객체(Entity)를 대상으로 전송한다는 점을 꼭 인지하고 넘어가야 한다.

파라미터를 받아올 때 @Param("name") String name 구문과 같이 @Param 어노테이션을 이용한 이유는 파라미터를 바인딩 할때 메서드의 파라미터의 이름을 지정하기 위함이다. @Param 어노테이션은 이름을 기반으로 지정되기 때문에 소스코드의 가독성을 보다 향상시킬 수 있다.

이제 마지막으로 앞에서 @Query 어노테이션을 통해 작성한 findMemberByName 메소드를 테스트해보자.

@Test
public void findMember() {
	String name = "lango";
    Member lango = memberRepository.findMemberByName(name);
    System.out.println(lango);
}

lango라는 name을 가지는 Member 객체를 찾아 lango라는 Member 타입 변수에 저장하고 출력하는 테스트코드를 작성하였다.

테스트 결과 lango라는 이름을 가지는 Member를 DB에서 잘 찾아오는 것을 확인할 수 있었다.

예제에서와 같이 이름을 통해 객체를 조회하는 작업은 JPA에서 제공하는 기본 find 메소드로 충분히 가져올 수 있지만 @Query 어노테이션과 JPQL의 이해도를 높이고자 간단하게 작성해보았다. 차후에 기본 메서드로 가져오기엔 복잡한 검색 작업 등을 구현할 때 @Query 어노테이션은 웬만하면 사용해야할 것으로 보인다.



Day - 57

Lombok의 @NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor

스프링 게시판 프로젝트를 실습하며 Lombok에서 지원하는 @NoArgsConstructor 어노테이션과 @RequiredArgsConstructor 어노테이션을 자주 붙여서 사용했었다. 두 가지 모두 생성자를 자동으로 제공해주는 어노테이션들인데 왜 클래스에 자주 붙여서 사용하는지 이해하고 넘어가기 위해 복습해보려 한다.

사살 저 @NoArgsConstructor, @RequiredArgsConstructor 어노테이션과 더불어 @AllArgsConstructor 어노테이션 3가지는 이름만 봐도 어떤 역할을 하는지 어느정도 유추할 수 있다. 이 어노테이션들이 어떤 의미를 가지고 어떻게 활용되는지 공식문서를 통해 알아보자.

@NoArgsConstructor

@NoArgsConstructor 어노테이션은 파라미터가 없는 기본 생성자를 만들어준다.

@NoArgsConstructor
public class Post {
	private Long postId;
    private String category;
    private String title;
    private String content;
    private Long likes;
}

다음과 같이 게시글을 의미하는 Post라는 객체가 있다고 해보자.

Post post = new Post();

그렇다면 위와 같이 파라미터를 넘기지 않고 생성자 호출을 할 수 있다.

만약 Post의 속성 중 final 키워드가 붙은 속성이 있을 경우, 해당 속성을 초기화할 수 없어서 생성자를 만들 수 없기 때문에 컴파일 에러가 발생하게 된다. 이 경우 @NoArgsConstructor(force = true) 구문과 같이 force라는 옵션을 이용하면 final로 선언된 속성을 0, false, null 등으로 초기화하여 생성자를 만들 수 있다.

또한, 특정 속성에 @NonNull과 같은 제약조건이 설정되어있는 경우, @NoArgsConstructor(force = true) 구문으로 옵션을 부여하여도 생성자를 만들 수 없기 때문에(null-check 로직이 생성되지 않음) 개발자가 별도의 작업을 해야한다.

@RequiredArgsConstructor

@RequiredArgsConstructor 어노테이션은 일부 파라미터를 필요로 하는, 즉 특별한 처리가 필요한 각 속성마다 하나의 파라미터를 가지는 생성자를 만들어준다. 구체적으로 이야기하자면, 초기화 되지 않은 final 속성들과 선언될 때 초기화되지 않은 @NonNull 어노테이션을 붙인 속성들에 대해서 생성자를 만들어준다.

@NonNull 어노테이션이 붙은 속성의 경우 생성되는 생성자에서 null-check 로직을 생성해주기 때문에 @NonNull 어노테이션이 붙은 속성 중 하나라도 null이 존재한다면 NullPointerException이 발생하게 된다.

그리고 파라미터의 순서는 클래스 내부에 선언된 속성들의 순서와 일치한다.

@RequiredArgsConstructor 어노테이션을 사용하는 방법은 다음과 같다.

@RequiredArgsConstructor
public class Post {
	@NotNull
	private Long postId;
    private String category;
    private String title;
    private String content;
    private Long likes;
}
Post post = new Post(1L);

위와 같이 파라미터에 @NotNull 어노테이션이 붙은 속성인 postId 속성의 파라미터를 전달하여 생성자를 호출하게 된다.

@AllArgsConstructor

@AllArgsConstructor 어노테이션은 클래스 내부에 선언된 모든 속성들마다 하나의 파라미터를 가지는 생성자를 만들어준다.

@RequiredArgsConstructor 어노테이션과 마찬가지로 @NonNull 어노테이션이 붙은 속성의 경우 생성되는 생성자에서 null-check 로직을 생성해준다.

@AllArgsConstructor 어노테이션을 사용하는 방법을 보자.

@AllArgsConstructor
public class Post {
	@NotNull
	private Long postId;
    private String category;
    private String title;
    private String content;
    private Long likes;
}
Post post = new Post(1L, "BASIC", "게시글 제목", "게시글 내용", 1L);

위와 같이 모든 필드를 파라미터로 전달하여 생성자를 호출하는 것을 볼 수 있다.

@RequiredArgsConstructor, @AllArgsConstructor 어노테이션의 심각한 문제점

@AllArgsConstructor@RequiredArgsConstructor는 심각한 문제를 발생시킬 수 있어서 사용을 권장하지 않는 경우도 있다고 한다. 어떤 심각한 문제가 있다는걸까?

이 문제를 이해하기 위해 하나의 예를 들어보자. 상품 주문과 연관된 Order라는 객체를 만들고 상품을 구매한 수량(buyCount)과 구매한 상품을 취소한 수량(cancelCount)을 나타내는 속성들을 선언해보자.

@AllArgsContructor
public class Order {
	private int buyCount; // 상품 구매수량
    private int cancelCount; // 상품 구매취소수량
}

그리고 Order의 생성자를 호출하는 구문을 작성한다.

Order order = new Order(2, 5);

이와 같이 Order의 생성자를 호출하게되면 클래스 내부에서 선언된 순서대로 buyCount, cancelCount 순으로 파라미터를 전달하여 생성자를 만들게 된다.

그런데 문제는 개발자가 해당 클래스의 코드를 변경했을 때 발생하게 된다.

@AllArgsContructor
public class Order {
    private int cancelCount;
	private int buyCount;
}

개발자의 재량으로 cancelCount, buyCount 순으로 코드가 변경된다면 생성자를 호출할 때 큰 문제가 발생하게 된다.

Order order = new Order(2, 5);

코드가 변경이 되었지만 이전과 동일하게 생성자를 호출하여도 문제 없이 정상적으로 동작하게 되지만, 실제로 생성자에게 전달해주는 파라미터 값은 cancelCount가 2, butCount가 5로 바뀌게 되어 심각한 문제가 발생한다는 것이다.

이 때문에 @RequiredArgsConstructor, @AllArgsConstructor 어노테이션을 통해서 생성자를 호출하는 것이 아니라 직접 생성자를 만들고 @Builder 어노테이션을 붙여서 생성자를 만들게 되면 위와 같은 문제를 방지할 수 있다고 한다.

@AllArgsContructor
public class Order {
    private int cancelCount;
	private int buyCount;
    
    @Builder
    private Order(int cancelCount, int buyCount) {
    	this.cancelCount = cancelCount;
        this.buyCount = buyCount;
    }
}

위 코드와 같이 @Builder 어노테이션을 통해 생성자를 만들어보자.

// buyCount=2, cancelCount=5
Order order = Order.builder().cancelCount(5).buyCount(2).build();

그렇다면 @RequiredArgsConstructor, @AllArgsConstructor 어노테이션을 사용할 때처럼 생성자를 호출할 때 파라미터 순서대로 만드는 것이 아니라 파라미터 이름으로 설정항 생성자를 만들기 때문에 개발자가 유지보수하는데 더욱 대처가 쉬울 것으로 보인다.







Final..

이번 주는 Spring Boot Project에서 File Upload, N:M 관계와 AOP, Spring Security를 배우고 간단한 실습 예제를 작성해보았다.

요즘 스프링을 배우면 배울수록 더 배울게 많아진다는 말을 몸소 체감하고 있다. 이와 더불어 가끔 생소한 Java의 스트림 객체까지 복습하려니 몸이 두개라도 모자란 상황인 것 같다.

그리고 스프링의 IoC와 DI, AOP 이 3가지 항목에 대해서 회고록 안에 내용을 작성할까 고민을 많이 했는데 스프링의 핵심 요소인만큼 별도의 포스팅으로 공부를 하고 싶은 마음이 크다. 카카오 클라우드 스쿨 기간 내에서 작성할 수 있다면 좋겠지만 양질의 글로 작성하고 싶은 마음이 커서(부담되서) 천천히 준비하여 작성하려 한다.

그래도 교육 과정 속에서 아예 이해가 안되거나 잘못 숙지하는 불상사는 없는 것 같아 다행이면서도 열정이 넘치는 동기 교육생들이나 대단하신 주니어, 시니어 개발자분들을 보면 너무나도 작은 사람임을 느끼곤 한다.

스프링 교육과정이 2주만에 끝이 났지만(너무나도 짧은시간이라고 생각한다.) 자바와 스프링은 꾸준히 챙겨가야할 숙제라고 생각하고 반복하고 반복할 생각이다.

이제 다음주 교육부터는 클라우드와 네트워크에 대해서 배우게 될 텐데 Docker와 kubernetes, CI/CD 등에 대해서 배우고 실습하면서 클라우드 환경에서 내가 만든 애플리케이션을 어떻게 다룰 것인가를 미리 고민해봐야 할 것 같다.



혹여 잘못된 내용이 있다면 지적해주시면 정정하도록 하겠습니다.

게시물과 관련된 소스코드는 Github Repository에서 확인할 수 있습니다.

참고자료 출처

profile
찍어 먹기보단 부어 먹기를 좋아하는 개발자

0개의 댓글