Open seungriyou opened 9 months ago
이전과는 다르게 tomcat 웹 서버를 임베디드 하고 있다.
build 시에는 다음의 과정으로 생성된 파일을 실행하기만 하면 된다.
./gradlew build
cd build/libs
java -jar hello-spring-0.0.1-SNAPSHOT.jar
# build 폴더 삭제
./gradlew clean
# build 폴더 삭제 후 재빌드
./gradlew clean build
정적 컨텐츠
서버에서 뭐 하는 것 없이 파일을 그대로 브라우저로 내려주는 것이다.
MVC와 템플릿 엔진
Model-View-Controller (모델, 템플릿 엔진, 화면)
서버에서 프로그래밍해서 HTML을 동적으로 바꾸어서 전달한다.
요즘에는 이 패턴으로 많이 개발한다.
API
JSON과 같은 데이터 구조로 클라이언트에게 데이터를 전달한다.
안드로이드, Vue, React 등을 쓸 때에도 API로 데이터만 내려주면 화면은 클라이언트가 알아서 그린다.
서버끼리만 통신할 때도 데이터만 주고 받으면 되기 때문에 사용한다.
[!TIP] 크게 HTML로 내리냐, API를 통해 데이터를 바로 내리냐 중에서 생각하면 된다.
스프링은 우선 hello-static
관련 컨트롤러를 먼저 찾아본다.
없으면 static
리소스에서 찾는다.
[!Note] 템플릿 엔진이 변환한 HTML 파일을 웹 브라우저에 반환한다.
http://localhost:8080/hello-mvc?name=spring!
[!Note] 데이터 그 자체를 반환한다. (view 필요 X)
컨트롤러에서 (1) 객체를 반환하면서 (2)
@ResponseBody
annotation을 달아둔다면 디폴트로 해당 데이터를 JSON으로 변환한다. (예전에는 xml 방식도 사용)
API 방식의 진가는 다음과 같이 객체 자체를 반환할 때에 알 수 있다.
@Controller
public class HelloController {
...
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
// static class로 만들면 class 안에서 또 class를 만들어서 사용할 수 있다.
static class Hello {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
다음의 URL로 접속하면 JSON 방식으로 데이터가 전달되었음을 확인할 수 있다.
http://localhost:8080/hello-api?name=spring!
스프링 컨테이너는 @ResponseBody
annotation을 통해 HTML 파일을 찾지 않고,
컨트롤러에서 반환되는 객체를 HTTP response body에 담아 데이터 자체를 전달한다.
이때, 디폴트로 JSON 방식으로 데이터를 만들어서 반환한다.
그러면 이전처럼 ViewResolver
가 동작하는 것이 아닌, HttpMessageConverter
가 동작한다.
StringConverter
가,JsonConverter
가동작하여 클라이언트로 데이터를 전달한다.
[!Tip] (1) 클라이언트의 HTTP Accept 헤더와 (2) 서버의 컨트롤러 반환 타입 정보를 조합해서
HttpMessageConverter
가 선택된다.
getter / setter를 통해 private
필드를 public
메서드를 통해 접근하도록 한다.
→ 프로퍼티 접근 방식
데이터베이스가 선정되지 않은 상황이므로 interface
로 구현 클래스를 변경할 수 있도록 설계한다.
(우선은 가벼운 메모리 기반의 데이터 저장소)
테스트 코드는 test/
디렉토리 밑에 작성하고, 각 메서드 위에는 @Test
annotation을 달아준다.
테스트 코드를 작성하는 방법은 주로 junit
또는 assertj
를 사용하는 것이다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
// 메서드 실행이 끝난 후 repository 데이터를 정리하도록 콜백 메서드 정의
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
// optional에서 값을 꺼낼 때는 get으로 꺼낼 수 있다. (좋은 방법은 아니지만 테스트 코드에서는 ok)
Member result = repository.findById(member.getId()).get();
/* <테스트 방법 #1> 직접 println 찍어보기 */
System.out.println("result = " + (result == member));
/* <테스트 방법 #2> junit의 Assertions 이용하기 */
Assertions.assertEquals(member, null);
/* <테스트 방법 #3> assertj의 Assertions 이용하기 (더 편리) */
// (option + enter) 치고 static import 해두면 편리하게 메서드 이름으로만 사용 가능
assertThat(member).isEqualTo(result);
}
...
}
테스트를 클래스 단위로 수행할 때, 실행되는 메서드 순서는 보장할 수 없으므로, 각 메서드에서 사용한 데이터를 정리해주어야 한다. 따라서 메서드를 서로 의존 관계가 없도록 작성해야 하며, 하나의 테스트가 끝날 때마다 저장소나 공용소의 데이터들을 다시 지워주어야 한다.
테스트를 먼저 만들고 구현 클래스를 만들면서 맞추는 것을 TDD라고 한다.
테스트 코드에 대해서는 더 자세히 공부해야 한다!
[!Tip] 보통 서비스는 비즈니스에 의존적으로 설계하고, 리포지토리는 개발 관련 용어를 사용한다.
→ 네이밍 시 유의하자.
Optional
객체를 반환 받는 경우, 굳이 assignment를 사용할 필요 없이 다음과 같이
ifPresent
등의 메서드를 사용하고작성하면 깔끔하다.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/**
* 회원 가입
*/
public Long join(Member member) {
/* 같은 이름이 있는 회원은 저장 X */
validateDuplicateMember(member); // 메서드를 따로 빼는 편이 낫다.
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
// 다음 결과는 optional로 반환되므로, 그 안에 값이 있으면 이미 이름이 같은 회원이 존재한다는 것 (ifPresent)
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
...
}
command
+ shift
+ t
단축키를 통해 편리하게 테스트 코드 템플릿을 생성할 수 있다.
테스트 코드를 작성할 때는 메서드 이름을 한글로 작성해도 된다.
다음의 구조를 따르면 테스트 코드를 작성하기 편리하다.
// given
// when
// then
테스트에서는 정상 플로우도 중요하지만 예외 플로우도 중요하다! (assertThrow
)
@Test
public void duplicated_member_exception() {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// when
memberService.join(member1);
// then
/* <방법 #1> try-catch 사용
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
*/
/* <방법 #2> assertThrow 사용 */
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
데이터를 마찬가지로 클리어 해주어야 하므로 리포지토리를 가져와야 한다.
class MemberServiceTest {
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
하지만, MemberService
에서 사용하는 repository와 MemberServiceTest
에서 사용하는 repository는 다른 객체이다. (new
로 생성했으므로) 즉, 다른 repository로 테스트하고 있는 것이다.
물론, 현재는
MemoryMemberRepository
의store
가static
으로 생성되어 class에 붙기 때문에 문제가 없지만, 그것이 아니라면 다른 DB를 사용하게 되는 것이다.
따라서 다음과 같이 MemberService
에 MemberRepository
를 외부에서 넣어주도록 변경한다.
MemberService.java
public class MemberService {
private final MemberRepository memberRepository;
// MemberRepository를 외부에서 넣어주도록 한다.
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
MemberServiceTest.java
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
// 각 테스트를 실행하기 전에 repository를 만들고, service에 이것을 넣어준다.
// 이렇게 하면 같은 repository를 사용한다.
// -> DI (Dependency Injection)
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
[!NOTE] DI (Dependency Injection)
어떤 서비스를 여러 컨트롤러에서 사용하려는 상황을 가정해보자. 만약 여러 컨트롤러의 구현 클래스에서 해당 서비스 인스턴스를 새롭게 생성한다면, 여러 컨트롤러에서 각기 다른 서비스 인스턴스를 사용하게 될 것이다.
어떤 리포지토리를 여러 서비스에서 사용하려는 상황에서도 마찬가지로, 여러 서비스에서 각기 다른 리포지토리 인스턴스를 사용하게 될 것이다.
[!Note] 이러한 이유로 스프링은 스프링 컨테이너에 컨트롤러, 서비스, 리포지토리를 스프링 빈으로 등록하고, 각 스프링 빈은 기본적으로 싱글톤이다. (특별한 경우가 아니면 대부분 싱글톤을 사용한다.) 따라서 스프링 컨테이너에 스프링 빈을 유일하게 하나만 등록하여 공유하는 것이므로, 같은 스프링 빈이면 같은 인스턴스이다.
컨트롤러, 서비스, 리포지토리
- 컨트롤러를 통해 외부 요청을 받고
- 서비스에서 비즈니스 로직을 만들고
- 리포지토리에서 데이터를 저장한다.
A(ex. 컨트롤러)에서 B(ex. 서비스)를 사용하기 위해 의존성 주입하려는 경우
B 클래스에 적절한 어노테이션을 달아준다.
@Controller
, 서비스 클래스에는 @Service
, 리포지토리 클래스에는 @Repository
어노테이션을 달아준다.A의 생성자에 @Autowired
어노테이션을 달아준다.
@Autowired
를 생략할 수 있다.@Autowired
어노테이션은 스프링 컨테이너에서 관리되는 객체에 대해서만 동작한다.@Repository
public class MemoryMemberRepository implements MemberRepository {}
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
@Component
어노테이션이 있으면 해당 인스턴스는 스프링 빈으로 자동 등록되는데,
@Controller
, @Service
, @Repository
어노테이션은 모두 @Component
어노테이션을 포함하므로 마찬가지로 스프링 빈으로 자동 등록되게 된다.
이때, XXXXApplication
클래스가 위치한 패키지 하위의 클래스들에 대해서만 컴포넌트 스캔이 이루어진다.
컨트롤러에는 그대로 @Controller
, @Autowired
어노테이션을 두고,
서비스, 리포지토리는 어노테이션을 삭제한 후 SpringConfig
클래스에서 직접 스프링 빈으로 등록해보자.
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()); // MemberRepository를 넣어주어야 하므로
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
A에 B를 주입하는 경우를 가정하자. A는
MemberController
, B는MemberService
가 된다.
생성자 주입: A의 생성자에 @Autowired
어노테이션 달기
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
필드 주입: 생성자 없이, 필드 선언 시에 앞에 @Autowired
어노테이션 달기
@Autowired private final MemberService memberService;
setter 주입: A에 B 필드에 대한 setter를 선언하고, @Autowired
어노테이션 달기
setter는 public으로 열어두어야 하는데, 이렇게 되면 해당 필드를 중간에도 바꿀 수 있게 되어 문제가 발생할 수도 있다. 하지만 조립 시점에 한 번 셋팅이 되고 나면 그 후에 바꿀 일은 없기 때문에 굳이 이렇게 할 이유가 없다.
private MemberService memberService; // final X
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
[!Tip] 의존 관계가 실행 중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다. 진짜 바뀌어야 할 때는 수정 후 다시 서버에 올린다.
주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔 방법을 사용한다.
하지만 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하는 경우(ex. 현재처럼 MemoryMemberRepository
를 추후에 다른 구현 클래스로 변경), 설정을 통해 스프링 빈으로 등록한다. (이렇게 하면 다른 코드의 수정 없이 설정 파일에서만 구현 클래스를 바꿔치기 할 수 있게 된다.)
https://www.h2database.com/html/main.html 에서 설치 파일 다운로드 및 압축 풀기
h2/bin
에서 ./h2.sh
실행
브라우저로 접속 (안 되면 IP 주소 부분을 localhost
로 변경)
JDBC URL
에 다음과 같이 입력 후 생성하고, test.mv.db
가 생성되었는지 확인
jdbc:h2:~/workspace/spring-study/spring-study/01-introduction/hello-spring/test
이후로는 여러 군데에서 동시 접속이 가능하도록 JDBC URL
에서 tcp://localhost/
를 추가하여 소켓을 통해 접속
jdbc:h2:tcp://localhost/~/workspace/spring-study/spring-study/01-introduction/hello-spring/test
JdbcMemberRepository
)옛날에는 이렇게 했었다. 코드는 강의 안에서 복사해와서 붙여 넣고, 자세히 볼 필요 없다.
build.gradle
에 추가 후 새로고침
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
...
}
application.properties
에 설정 정보 추가
spring.datasource.url=jdbc:h2:tcp://localhost/~/workspace/spring-study/spring-study/01-introduction/hello-spring/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
SpringConfig
에서 MemberRepository
부분을 MemoryMemberRepository
에서 JdbcMemberRepository
로 변경한다.
이때, 필요한 DataSource
는 마찬가지로 스프링 빈으로 관리되므로 주입 받기 위해 생성자 주입 + @Autowired
를 사용한다.
이렇게 함으로써 다른 코드는 전혀 수정할 필요 없이 설정만으로 기능을 변경할 수 있는 것이다.
@Configuration
public class SpringConfig {
// DataSource는 application.properties에 적힌 값을 토대로 스프링 빈으로 생성 및 관리되기 때문에
// 마찬가지로 DI 해준다. (생성자 주입 + @Autowired)
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository()); // MemberRepository를 넣어주어야 하므로
}
@Bean
public MemberRepository memberRepository() {
return new JdbcMemberRepository(dataSource);
}
}
[!Note] 스프링 컨테이너는 객체지향적인 설계, 의존성 주입 등을 편리하게 지원해준다.
→ 개방 폐쇄 원칙 (OCP, Open-Closed Principle): 확장에는 열려있고, 수정/변경에는 닫혀있다.
이전까지는 순수 자바 코드 테스트였으나, 이제는 데이터베이스 커넥션 정보도 스프링 부트가 들고 있기 때문에 스프링 통합 테스트를 수행해야 한다.
[!Tip]
- 테스트 코드이므로 의존성 주입 받을 때, 그냥 편하게 필드 주입을 받아도 된다.
beforeEach
,afterEach
대신@Transactional
을 사용할 수 있다.
@SpringBootTest
: 스프링 컨테이너와 테스트를 함께 실행한다.
@Transactional
: 테스트 케이스에 달면 테스트를 실행할 때 트랜잭션을 시작하고, 테스트 완료 후에 항상 rollback 해준다. 이렇게 하면 DB에 반영이 되지 않아 다음 테스트에 영향을 주지 않는다.
@Test
어노테이션 및에 @Commit
어노테이션도 추가한다면 DB에 반영이 된다.[!Note] 테스트의 종류
단위 테스트: 순수한 Java 코드로, 최소한의 단위로 수행하는 테스트
→ 잘 할 수 있도록 훈련해야 한다! (좋은 테스트일 확률이 높다.)
통합 테스트: DB, 스프링 컨테이너까지 연동하는 테스트
JdbcTemplateMemberRepository
)설정은 순수 JDBC와 동일하다.
스프링 JdbcTemplate이나 MyBatis 같은 라이브러리는 JDBC API에서 등장했던 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다.
실무에서 실제로 많이 쓴다.
생성자가 하나이면 @Autowired
를 사용할 필요가 없다.
마찬가지로 SpringConfig
에서 JdbcTemplateMemberRepository
로 변경해주면 된다.
다음과 같이 DataSource
를 주입 받아 생성자에서 생성한 JdbcTemplate
객체를 사용한다.
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
// 생성자가 하나이면 @Autowired를 사용할 필요가 없다.
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
이전에 작성해둔 통합 테스트 코드로 확인해보자.
JpaMemberRepository
)build.gradle
추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
application.properties
추가
JPA는 표준 인터페이스이고, Hibernate는 구현체이다.
spring.jpa.show-sql=true // JPA가 생성하는 SQL문 출력
spring.jpa.hibernate.ddl-auto=none // 객체를 기반으로 테이블을 자동으로 생성하는 기능 off
도메인 엔티티 클래스 위에 @Entity
어노테이션, id 필드 위에 @Id
, @GeneratedValue
어노테이션을 추가한다. (→ JPA가 관리하는 엔티티)
DB에 데이터를 넣으면 id와 같은 값을 DB 자동으로 생성해주는 방식을 아이텐티티 전략이라 한다.
package hello.hellospring.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
만약, 대응되는 테이블 컬럼 이름과 엔티티 클래스 필드 이름이 다르다면, 다음과 같이 @Column
어노테이션을 통해 테이블 컬럼 이름을 명시해주면 된다.
@Column(name = "username")
private String name;
서비스 계층에 @Transactional
어노테이션을 달아야 한다.
JPA는 데이터를 저장 및 변경할 때 항상 트랜잭션으로 동작해야 한다.
@Transactional
public class MemberService {
EntityManager
로 작동하므로, 이를 생성자에서 주입 받는다.
스프링 부트가 자동으로
EntityManager
를 생성 및 데이터베이스와 연결해준다. 이를 주입 받기만 하면 된다.
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
PK 기반이 아닌 기능에 대해서는 JPQL이라는 쿼리 언어를 사용하면 엔티티 객체를 대상으로 쿼리를 실행할 수 있다. (→ “객체 자체”를 select 가능)
기존의 SQL 코드에서는 그 결과를 다시 엔티티 객체로 mapping 해야 했다.
@Override
public Optional<Member> findByName(String name) {
List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
return result.stream().findAny(); // optional로 반환하기 위해서
}
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class).getResultList();
}
이전에 작성해둔 통합 테스트 코드로 확인해보자.
SpringConfig
)에서 구현체 변경@Configuration
public class SpringConfig {
// Jdbc, JdbcTemplate에서는 DataSource를 DI 해주었으나,
// JPA에서는 EntityManager를 DI 해준다. (생성자 주입 + @Autowired)
// EntityManager는 DataSource와 마찬가지로 application.properties에 적힌 정보를 토대로 스프링 빈으로 생성 및 관리된다.
private EntityManager em;
@Autowired
public SpringConfig(EntityManager em) {
this.em = em;
}
...
@Bean
public MemberRepository memberRepository() {
return new JpaMemberRepository(em);
}
}
SpringDataJpaMemberRepository
)스프링 데이터 JPA를 사용하면 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있다.
메서드 이름만으로 조회 기능을 제공하는 등 편리하다.
인터페이스만으로 기본 CRUD 기능을 제공한다.
단순하고 반복적인 코드를 줄일 수 있으므로, 핵심 비즈니스 로직 개발에 집중할 수 있다.
페이징 기능을 자동으로 제공한다.
[!Note] 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술이므로, JPA를 먼저 학습해야 한다.
JpaRepository
와 MemberRepository
를 다중 상속 받도록(extends) 하도록 하고 다음과 같이 작성한다.
JpaRepository
를 상속 받고 있으면 스프링 데이터 JPA가 구현체를 자동으로 만들어 스프링 빈에 등록해준다.
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
}
SpringConfig
)에서 구현체 변경MemberRepository
를 상속 받고 있는 리포지토리 인터페이스의 구현체가 스프링 데이터 JPA에 의해 이미 스프링 빈으로 등록되어 있다.
따라서 SpringConfig
에서 새로 생성 및 @Bean
으로 등록할 필요 없이, 생성자로 주입만 받으면 된다.
@Configuration
public class SpringConfig {
// MemberRepository를 상속 받고 있는 리포지토리 인터페이스의 구현체가 스프링 데이터 JPA에 의해 이미 스프링 빈으로 등록되어 있다.
// 따라서 SpringConfig에서 새로 생성 및 @Bean으로 등록할 필요 없이, 생성자로 주입만 받으면 된다.
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository); // MemberRepository를 넣어주어야 하므로
}
}
JpaRepository
간략히 살펴보기기본적인 메서드(save
, findById
, findAll
, 기본 CRUD 등)들이 제공된다. 따라서 가져다 쓰기만 하면 된다.
하지만, PK로 찾는 것이 아닌 경우라면 이러한 공통 클래스로 구현할 수 없다. 바로 이러한 경우에 다음과 같이 @Override
하여 선언하는 것이다.
@Override
Optional<Member> findByName(String name);
여기에는 규칙이 있는데, 위와 같이 메서드 이름을 findByName
이라고 설정하면, 다음과 같은 JPQL로 변환된다.
"select m from Member m where m.name = ?"
예를 들어 메서드 이름을 findByNameAndId
라고 설정한다면 name과 id를 인자로 받아 select 하는 JPQL로 변환하게 된다.
[!Tip] 스프링 데이터 JPA를 통해 인터페이스 상에서 이름만으로 개발이 끝난다. 물론, 많이 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하여 해결한다. 혹은 JPA가 제공하는 네이티브 쿼리나 JdbcTemplate, MyBatis를 사용할 수도 있다.
(실무에서는 대부분 스프링 데이터 JPA + Querydsl 조합으로 사용한다.)
핵심 관심 사항과 공통 관심 사항(ex. 메서드별 시간 측정 로직)을 분리하는 방법이다.
이러한 사항들과 핵심 비즈니스 로직이 섞이게 되면 유지보수가 어렵고, 이를 별도의 공통 로직으로 만들기 어렵다.
aop
패키지를 만들어서 TimeTraceAop
를 작성한다. 이때, 클래스에 @Aspect
어노테이션을 달아준다.
스프링 빈에 등록하기 위해 컴포넌트 스캔 방식을 사용할 수도 있으나(@Component
어노테이션), AOP임을 명확하게 명시하기 위해 SpringConfig
에 직접 스프링 빈에 등록하는 방식을 권장한다.
🤔 이렇게 하니까 순환 의존 에러가 발생하는 것 같다.. 우선은 그냥
@Component
로!
TimeTraceAop
의 메서드에 @Around()
어노테이션을 통해 공통 관심 사항을 타겟팅한다.
[!NOTE]
- AOP를 통해 다음 메서드 호출 시마다 중간에서 인터셉트 해서 동작을 수행할 수도 있다.
- 보통
@Around
에는 패키지 단위로 설정한다.
AOP를 적용한 후의 컨트롤러와 서비스 간 의존 관계는 다음과 같다.
즉, memberController
는 실제 memberService
가 아닌 프록시 memberService
객체를 주입 받으며, joinPoint.proceed()
코드를 만나야지만 실제 memberService
가 사용되게 된다.
이러한 방식을 “프록시 방식의 AOP”라고 하며, 실제로 프록시 객체를 콘솔에 출력해서 확인할 수 있다.
Contents