스프링입문-by-김영한님
2021년08월23일김영한님의 강의를 듣고 정리하는 글이다.
프로젝트 환경 설정
요새는 스프링부트로 많이 시작하는데 start.spring.io 에서 적절한 dependecies와 자바 버전, 빌드 도구 등을 선택해서 만들어 주면 된다.
스프링 웹 개발 기초
정적 컨텐츠
- 정적 컨텐츠: 정적인 html파일을 던져주어 변화가 없다.
- MVC와 템플릿 엔진: 서버 내에서 템플릿 엔진을 통해 동적으로 변환한 값을 준다.
- API: JSON형태로 데이터를 클라이언트에 보내준다. 그러면 클라이언트 측에서 알아서 렌더링하게 처리해준다.
정적 파일은 resources의 static 디렉터리에서 찾아서 출력해준다. 그래서 url/파일명.html을 하면 해당 정적 파일이 출력이 가능하다.
MVC와 템플릿 엔진
MVC: Model, View, Controller 예전에는 JSP에다가 통으로 넣어서 관리했었는데, 이러면 관리 측면에서 많이 어려워지기에 위의 3가지 형식으로 분리해서 관리하게 되었다.
API
이전에 했던 방식은 데이터를 html파일에서 렌더링해서 보여주는 방식이고 이번에는 API 방식으로 데이터를 보내는 방식이다. 이 방식을 쓰면 JSON 으로 반환해준다. 이때는 @ResponseBody 어노테이션을 쓰면 된다.
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
static class Hello {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
회원 관리 예제 - 백엔드 개발
비즈니스 요구사항 정리
- 데이터: 회원 아이디, 이름
- 기능: 회원 등록, 조회
일반적인 웹 어플리케이션 계층 구조
- 컨트롤러: 웹 MVC의 컨트롤러 역할
- 서비스: 핵심 비즈니스 로직 구현
- 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨.
// 저장소 인터페이스
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
// 구현체
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
}
회원 리포지토리 테스트 케이스 작성
개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해 실행하거나, 웹 애플리케이션의 컨트롤러를 통해 해당 기능을 실행한다. 근데 이런 방법들은 준비라던가 실행시간이 오래 걸리고 반복하거나 한 번에 테스트하기 어렵다. 이런 문제점을 jUnit이라는 프레임워크로 해결한다.
// test/java/hello/hellospring/repository 관례적으로 main 디렉터리랑 같은 구조로 만들어 준다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
assertThat(member).isEqualTo(result);
}
@Test
public void findByName() {
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
Member result = repository.findByName("spring1").get();
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
@AfterEach 어노테이션은 각 테스트가 끝나면 해당 메서드가 실행되게끔 한다. 테스트를 전체적으로 실행하면 우리가 위 처럼 짠 코드 순서대로 실행되는게 아니라 jUnit이 실행하기에 이전에 했던 내용으로 충돌될 수도 있다. 그렇기에 위에서는 사용했던 내용을 초기화 해주도록 해준 것이다.
회원 서비스 개발
service 디렉터리를 만들어서 파일을 관리해주자. 서비스는 실제 비즈니스에 사용될 로직이기에 메서드 명들을 비즈니스적으로 작명해주는 것이 좋다. 회원가입 같은 경우에는 join 이런식으로.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/*
회원 가입
* */
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/*
전체 회원 조회
* */
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
회원 서비스 테스트
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외() {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// try {
// memberService.join(member2);
// fail();
// } catch (IllegalStateException e) {
// assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// }
// then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
테스트의 구조는 given, when, then 이렇게 세 가지로 나누어서 연습해보는 것이 좋다.
- given: 데이터 등을 주어줌.
- when: 메서드 등을 동작시킴.
- then: 어떤 테스트를 할 것인지 정함.
그리고 MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
이 부분에서 Repository는 MemberSerivce에서도 생성되고 지금의 memberRepository에서도 있는데 이것들은 서로 다른 객체이다. 이렇게 되면 문제점이 발생할 수 있기에 아래처럼 바꾸어 보자.
> 레포지토리의 private static Map<Long, Member> store = new HashMap<>(); 부분이 static이라서 위 코드는 문제없이는 돌아갈 것이다.
// MemberService
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// MemberServiceTest
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
MemberService에서 생성자를 만들어 인자로 레포지토리를 넣어줬다. 이런식으로 하는것이 DI, 의존성 주입이라고 한다.
스프링 빈과 의존 관계
컴포넌트 스캔과 자동 읜존관계 설정
스프링은 컨테이너를 만들고 빈 객체를 만드는데 이를 위해 클래스에 특별한 어노테이션들을 달아준다.
- @Component 어노테이션이 있고 이름 포함한 어노테이션들이 있다.
- @Controller, @Service, @Repository
클래스 위에 위와 같은 어노테이션을 달아주면 스프링 시작 시 빈 객체를 만들어 준 후 이를 사용하려면 @Autowired라는 어노테이션을 통해 연결해주면 된다.
package hello.hellospring.controller;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class MemberController {
private MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
생성자에 걸어도 되고 필드에다가도 걸어주면 된다. 참고로 스프링 어플리케이션이 위치한 패키지 내에서만 위 어노테이션들을 스캔해주기에 다른 패키지에 달아두면 안 된다.
자바 코드로 직접 스프링 빈 등록하기
위의 컴포넌트 어노테이션을 달지 않고 자바 코드로 빈 객체로 만드는 방법이 있다.
//SpringConf.java
package hello.hellospring;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
예전에는 XML로 설정을 했었다는데 요새는 쓰이지 않는다고 한다. 의존성 주입(DI)은 필드 주입, setter 주입, 생성자 주입 이렇게 3가지가 있는데, 의존관계가 실행중에 동적으로 변하는 경우는 거의 없기에 생성자 주입을 권장한다. 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다. 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록하는 것이 좋다. 주로 DB 설정 관련해서 변경할 때 사용된다고 한다.
회원 관리 예제 - 웹 MVC 개발
회원 웹 기능 - 홈 화면 추가
package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<h1>Hello Spring</h1>
<p>회원 기능</p>
<p>
<a href="/members/new">회원 가입</a>
<a href="/members">회원 목록</a>
</p>
</div>
</div> <!-- /container -->
</body>
</html>
아무 경로 없이 주소만 치면 index.html이 나와야할텐데 왜 위의 화면이 나오는 이뉴는 컨트롤러가 정적 파일보다 우선순위가 높기 때문이다. 만약 컨틀롤러가 없으면 정적파일이 나올 것이다.
회원 웹 기능 - 등록
// memberForm 생성
package hello.hellospring.controller;
public class MemberForm {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@GetMapping("/members/new")
public String createForm() {
return "members/createMemberForm";
}
@PostMapping("/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<form action="/members/new" method="post">
<div class="form-group">
<label for="name">이름</label>
<input type="text" id="name" name="name" placeholder="이름을입력하세요">
</div>
<button type="submit">등록</button>
</form>
</div> <!-- /container -->
</body>
</html>
PostMapping 메서드 쪽을 보면 매개변수로 MemberForm을 받는거를 볼 수 있는데 위에 있는 html 파일을 보면 post 요청으로 key값이 name인 값을 스프링으로 보낸다. 스프링은 key 값을 통해 setter 메서드를 불러 넣어주게 되고 이를 활용할 수 있게 된다.
회원 웹 기능 - 조회
@GetMapping("/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<table>
<thead>
<tr>
<th>#</th>
<th>이름</th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
모델 인스턴스에 members 인스턴스를 추가해주었다. 이를 thymeleaf 엔진과 함께 활용하여 동적으로 데이터가 추가되게끔 했다. 지금까지는 메모리 상에서 값이 추가되기에 서버를 재시작하면 다 날라간다. 이를 해결하기 위해 DB를 설정해보자!
스프링 DB 접근 기술
H2 데이터베이스 설치
H2 데이터베이스는 개발이나 테스트 용도로 가볍고 편리한 DB이다. 웹 관리자 화면도 제공해준다.
- https://www.h2database.com 본인의 맞는 OS로 다운로드 및 설치
- h2 데이터베이스 버전은 스프링 부트 버전에 맞춘다.
- 권한 주기: chmod 755 h2.sh (윈도우 사용자는 x)
- 실행: ./h2.sh (윈도우 사용자는 h2.bat)
- 앞의 주소를 localhost로 바꿔서 접속하면 된다.
데이터베이스 파일 생성 방법은 아래와 같다.
- jdbc:h2:~/test (최초 한번)
- ~/test.mv.db 파일 생성 확인
- 이후부터는 jdbc:h2:tcp://localhost/~/test 이렇게 접속 (파일로 접근하면 어플리케이션하고 웹 콘솔 충돌날 수도 있음)
이제 테이블을 생성해보자
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
순수 JDBC
애플리케이션을 통해 DB를 연결하여 데이터들을 저장하고 관리해보자. 먼저 20년된 방식인 순수 JDBC로 해보자…! 먼저 build.gradle에 라이브러리들을 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
그러고 디비에 접근하기 위한 접속 정보 (ID라던가 url)를 입력해야되는데, 예전에는 엄청 노가다였다한다. 지금은 resources/application.properties
에다가 입력해주면 된다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
영한님이 제공하는 코드를 적용 후 설정파일에서 레포지토리를 갈아끼워보자!
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
우리는 이렇게만 바꿔줘도 다른 내용은 고칠 필요없이 그대로 사용할 수 있다. 이게 바로 자바의 다형성과 스프링의 힘인거 같다. 위와 같이 수정없이 사용하는 원칙이 SOLID 중에 O, OCP(Open-Closed Principle)이다. 이 원칙은 확장에는 열려있고, 수정, 변경에는 닫혀있다는 의미이다.
스프링 통합 테스트
기존 테스트에서는 순수 자바 코드로만 이루어졌기에 스프링 내용들 없이도 할 수 있었다. 하지만 이제 DB도 연결했고 이것을 사용하는 빈 객체들을 사용하기 위해서는 스프링 컨테이너를 만들어서 테스트 해야한다. 이를 위한 어노테이션이 @SpringBootTest
이다. 이와 함께 @Transactional
이 사용되는데, 테스트는 한 번만 실행하는게 아니라 계속 사용할 수 있어야하기에 디비 내용을 롤백 시켜줘야 한다. 이 어노테이션을 활용하면 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백해준다. 클래스에 적용하면 모든 테스트마다 적용된다.
스프링 JdbcTemplate
- 순수 Jdbc와 동일한 환경설정을 해주면 된다.
- 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해주어야 한다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
생성자가 아나일 때는 Autowired 생략가능하다.
JPA
JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다. JPA를 사용하면 SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다. JPA는 인터페이스이고 Hibernate라는 구현체를 거의 표준으로 사용한다고 한다.
gradle에 라이브러리를 추가해주자
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
application.properties에도 다음과 같이 추가하자
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
첫 번째는 쿼리를 보여주는 설정이고, 두 번째는 jpa는 Entity를 설정하는데 이 엔티티를 통해 자동으로 테이블을 만들어 주려하기 때문에 꺼두는 설정이다.
Member 클래스를 수정해보자
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity 어노테이션을 달아주어 Entity임을 명시해주고, @Id는 해당 필드가 PK임을 알려주는 어노테이션이다. @GeneratedValue는 PK 값을 어떻게 설정해주느냐인데, IDENTITY 전략으로 사용한다고 설정해준다. 이를 쓰면 PK값을 잘 올려준다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = em.createQuery("select m from Member as m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
}
스프링 설정을 바꿔주자
@Configuration
public class SpringConfig {
private DataSource dataSource;
private EntityManager em;
@Autowired
public SpringConfig(DataSource dataSource, EntityManager em) {
this.dataSource = dataSource;
this.em = em;
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
return new JpaMemberRepository(em);
}
}
스프링 데이터 JPA
스프링 부트와 JPA만 사용해도 개발 생산성이 정말 많이 증가하고, 개발해야할 코드가 확연히 줄어든다. 여기서 스프링 데이터 JPA를 사용하면 리포지토리에 구현 클래스 없이 인터페이스만
으로 개발을 완료할 수 있다. 그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공한다.
이를 활용하여 개발자는 핵심 비즈니스 로직을 개발하는데 집중할 수 있다. 실무에서 관계형 데이터베이스를 사용한다면 스프링 데이터 JPA는 필수이다.
스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술이기에 JPA를 잘 이해해서 사용하자.
스프링 데이터 JPA는 인터페이스만 만들어 주면 될 정도로 놀라운 기술이다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
// JPQL select m from Member m where m.name = ?
@Override
Optional<Member> findByName(String name);
}
이런식으로 인터페이스만 만들어주면은 스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링 빈으로 자동 등록해준다. 이 스프링 데이터 JPA는 기본적인 CRUD를 제공해주고, findByName()이나 findByEmail 같은 것들을 메서드로만 추가해줘도 잘 동작해준다. 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, JdbcTemplate를 사용하면 된다.
AOP
Aspect Oriented Programming의 약자로 공통 관심사항과 핵심 관심 사항을 분리하는 것이다. 가령 우리는 매 메서드마다 시간 측정하고 싶은 기능을 넣으려고 한다. 근데 이것은 사실 핵심 기능은 아니다. 이런 기능을 공통 관심 사항으로 두고 따로 빼서 적용을 하면 되는 것이다.
package hello.hellospring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TimeTraceAop {
@Around("execution(* hello.hellospring..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("START: " + joinPoint.toString());
try {
return joinPoint.proceed();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
}
}
}
@Aspect 어노테이션은 AOP를 위해 사용하는 어노테이션이다. @Around 어노테이션은 어느 클래스에 적용할 것인지 설정하는 것이다. ProceedingJoinPoint는 실제 메서드(?)를 실행할 때 사용되는 것이라 보면 된다. 이렇게 만들어 두면 스프링은 프록시 기능을 활용하여 작동시키게 된다.