안녕하세요, 어제 작성하던 예제에서 다양한 어노테이션과 설정 방법을 알아보며 스프링에 대해 자세히 이해하는 시간을 가졌습니다.
함께 사용할 설정 클래스를 지정.
// AppConf1.java
package config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import ex01.MemberDAO;
import ex01.MemberPrinter;
@Configuration
@Import(AppConf2.class)
//@Import({ AppConf2.class, AppConf3.class })
public class AppConf1 {
@Bean
public MemberDAO memberDAO() {
return new MemberDAO();
}
@Bean
public MemberPrinter memberPrinter() {
return new MemberPrinter();
}
}
// MainForSpring.java
public class MainForSpring {
private static ApplicationContext ctx = null;
public static void main(String[] args) throws IOException {
// ctx = new AnnotationConfigApplicationContext(AppCtx.class);
// ctx = new AnnotationConfigApplicationContext(AppConf1.class, AppConf2.class);
ctx = new AnnotationConfigApplicationContext(AppConf1.class);

설정파일에 해당 내용의 빈이 있는지 본다.

해당 타입의 빈 객체가 한 개만 존재하는 경우, 빈 타입만으로 빈을 구할 수 있음


@Configuration
public class AppConf2 {
@Autowired
private MemberDAO memberDAO;
@Autowired
private MemberPrinter memberPrinter;
... (생략) ...
@Bean
public VersionPrinter versionPrinter() {
VersionPrinter versionPrinter = new VersionPrinter();
versionPrinter.setMajorVersion(5);
versionPrinter.setMinorVersion(3);
return versionPrinter;
}
@Bean
public VersionPrinter newVersionPrinter() {
VersionPrinter versionPrinter = new VersionPrinter();
versionPrinter.setMajorVersion(6);
versionPrinter.setMinorVersion(1);
return versionPrinter;
}
}
// MainForSpring.java
private static void processVersionCommand(String[] args) {
VersionPrinter versionPrinter = ctx.getBean(VersionPrinter.class);
versionPrinter.print();
}

사용 객체에서 의존 객체 필드에 @Autowired 어노테이션을 붙이면 스프링이 자동으로 해당 타입의 빈 객체를 찾아서 필드에 할당
// ChangePasswordService.java
package ex01;
import org.springframework.beans.factory.annotation.Autowired;
public class ChangePasswordService {
@Autowired
private MemberDAO memberDAO;
/* 의존 주입을 위한 코드는 불필요
public ChangePasswordService(MemberDAO memberDAO) {
this.memberDAO = memberDAO;
}
*/
public void changePassword(String email, String currentPw, String newPw) {
Member member = memberDAO.selectByEmail(email);
if (member == null) {
throw new RuntimeException("등록된 회원이 없습니다.");
}
member.changePassword(currentPw, newPw);
memberDAO.update(member);
}
}
@Autowired 어노테이션을 setter 메서드에 붙이면, 스프링이 해당 setter 메서드를 통해 의존성을 주입
// MemberInfoPrinter.java
package ex01;
import org.springframework.beans.factory.annotation.Autowired;
public class MemberInfoPrinter {
// @Autowired
private MemberDAO memberDAO;
// @Autowired
private MemberPrinter printer;
@Autowired
public void setMemberDAO(MemberDAO memberDAO) {
this.memberDAO = memberDAO;
}
@Autowired
public void setMemberPrinter(MemberPrinter printer) {
this.printer = printer;
}
public void printMemberInfo(String email) {
Member member = memberDAO.selectByEmail(email);
if (member == null) {
System.out.println("일치하는 데이터가 없습니다.");
return;
}
printer.print(member);
System.out.println();
}
}
//App.ctx
@Bean
public ChangePasswordService changePwdSvc() {
//return new ChangePasswordService(memberDAO());
return new ChangePasswordService();
}
@Bean
public MemberInfoPrinter memberInfoPrinter() {
MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
// infoPrinter.setMemberDAO(memberDAO());
// infoPrinter.setMemberPrinter(memberPrinter());
return infoPrinter;
}
// AppCtx.java
@Bean
public ChangePasswordService changePwdSvc() {
// return new ChangePasswordService(memberDAO());
return new ChangePasswordService();
}
// MainForSpring.java
ctx = new AnnotationConfigApplicationContext(AppCtx.class);
// ctx = new AnnotationConfigApplicationContext(AppConf1.class, AppConf2.class);
// ctx = new AnnotationConfigApplicationContext(AppConf1.class);
// MemberListPrinter.java
package ex01;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
public class MemberListPrinter {
@Autowired
private MemberDAO memberDAO;
@Autowired
private MemberPrinter printer;
// public MemberListPrinter(MemberDAO memberDAO, MemberPrinter printer) {
// this.memberDAO = memberDAO;
// this.printer = printer;
// }
public void printAll() {
Collection<Member> members = memberDAO.selectAll();
members.forEach(member -> printer.print(member));
}
}
// AppCtx.java
@Bean
public MemberListPrinter memberListPrinter() {
// return new MemberListPrinter(memberDAO(), memberPrinter());
return new MemberListPrinter(); ⇐ 의존객체를 매개변수로 가지는 생성자가 삭제되었기
} 때문에 기본 생성자로 변경
// @Bean ⇐ MemberListPrinter에서 @Autowired 대상 객체를
public MemberDAO memberDAO() { 빈으로 등록하지 않음
return new MemberDAO();
}

// AppCtx.java
@Bean
public MemberDAO memberDAO() {
return new MemberDAO();
}
@Bean
public MemberPrinter memberPrinter() {
return new MemberPrinter();
}
@Bean
public MemberPrinter memberPrinter2() {
return new MemberPrinter();
}
// MemberListPrinter.java
package ex01;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
public class MemberListPrinter {
@Autowired
private MemberDAO memberDAO;
@Autowired
private MemberPrinter printer;
// public MemberListPrinter(MemberDAO memberDAO, MemberPrinter printer) {
// this.memberDAO = memberDAO;
// this.printer = printer;
// }
public void printAll() {
Collection<Member> members = memberDAO.selectAll();
members.forEach(member -> printer.print(member));
}
}

@Autowired 어노테이션에 required 속성을 false로 설정
// MemberPrinter.java
public class MemberPrinter {
@Autowired(required = false)
private DateTimeFormatter dateTimeFormatter;
public void print(Member member) {
if (dateTimeFormatter == null) {
System.out.printf("회원정보: ID=%s, 이메일=%s, 이름=%s, 등록일=%tF\n",
member.getId(), member.getEmail(), member.getName(),
member.getRegisterDateTime());
} else {
System.out.printf("회원정보: ID=%s, 이메일=%s, 이름=%s, 등록일=%tF\n",
member.getId(), member.getEmail(), member.getName(),
dateTimeFormatter.format(member.getRegisterDateTime()));
}
}
}
자동 주입 가능한 빈이 두 개 이상이면 자동 주입할 빈을 지정할 방법이 필요 ⇒ @Qualifier 어노테이션을 이용해서 자동 주입 대상 빈을 한정.
// AppCtx.java
@Bean
@Qualifier("printer")
public MemberPrinter memberPrinter() {
return new MemberPrinter();
}
@Bean
public MemberPrinter memberPrinter2() {
return new MemberPrinter();
}
// MemberListPrinter.java
public class MemberListPrinter {
@Autowired
private MemberDAO memberDAO;
@Autowired
@Qualifier("printer")
private MemberPrinter printer;
// MemberInfoPrinter.java
public class MemberInfoPrinter {
// @Autowired
private MemberDAO memberDAO;
// @Autowired
private MemberPrinter printer;
@Autowired
public void setMemberDAO(MemberDAO memberDAO) {
this.memberDAO = memberDAO;
}
@Autowired
@Qualifier("printer")
public void setMemberPrinter(MemberPrinter printer) {
this.printer = printer;
}
빈 설정에 @Qualifier 어노테이션을 사용하지 않으면 빈 이름이 기본 한정자로 지정.
@Bean
public MemberPrinter memberPrinter2() { ⇐ memberPrinter2 = 빈 이름 = 기본 한정자
return new MemberPrinter();
}
@Autowired 어노테이션도 @Qualifier 어노테이션이 없으면 필드나 파라미터 이름을 한정자로 사용.
@Autowired
private MemberPrinter printer; ⇐ printer = 필드 이름 = 기본 한정자
@Qualifier 어노테이션을 이용해서 주입할 빈을 한정
// AppCtx.java
@Bean
@Qualifier("printer")
public MemberPrinter memberPrinter() {
return new MemberPrinter();
}
@Bean
@Qualifier("summaryPrinter")
public MemberSummaryPrinter memberPrinter2() {
return new MemberSummaryPrinter();
}
// MemberListPrinter.java
public class MemberListPrinter {
@Autowired
private MemberDAO memberDAO;
@Autowired
@Qualifier("summaryPrinter")
private MemberPrinter printer;
자식 클래스 타입의 빈을 자동 주입 받도록 수정
// AppCtx.java
@Bean
@Qualifier("printer")
public MemberPrinter memberPrinter() {
return new MemberPrinter();
}
@Bean
// @Qualifier("summaryPrinter")
public MemberSummaryPrinter memberPrinter2() {
return new MemberSummaryPrinter();
}
// MemberListPrinter.java
public class MemberListPrinter {
@Autowired
private MemberDAO memberDAO;
@Autowired
// @Qualifier("summaryPrinter")
private MemberSummaryPrinter printer;
자동 주입할 빈이 존재하면 해당 빈을 전달하고, 존재하지 않으면 null을 전달
public class MemberPrinter {
@Autowired
@Nullable
private DateTimeFormatter dateTimeFormatter;
public void print(Member member) {
if (dateTimeFormatter == null) {
System.out.printf("회원정보: ID=%s, 이메일=%s, 이름=%s, 등록일=%tF\n",
member.getId(), member.getEmail(), member.getName(),
member.getRegisterDateTime());
} else {
System.out.printf("회원정보: ID=%s, 이메일=%s, 이름=%s, 등록일=%tF\n",
member.getId(), member.getEmail(), member.getName(),
dateTimeFormatter.format(member.getRegisterDateTime()));
}
}
}
또는
public class MemberPrinter {
private DateTimeFormatter dateTimeFormatter;
@Autowired
@Nullable
public void setDateTimeFormatter(DateTimeFormatter dateTimeFormatter) {
this.dateTimeFormatter = dateTimeFormatter;
}
public void print(Member member) {
if (dateTimeFormatter == null) {
System.out.printf("회원정보: ID=%s, 이메일=%s, 이름=%s, 등록일=%tF\n",
member.getId(), member.getEmail(), member.getName(),
member.getRegisterDateTime());
} else {
System.out.printf("회원정보: ID=%s, 이메일=%s, 이름=%s, 등록일=%tF\n",
member.getId(), member.getEmail(), member.getName(),
dateTimeFormatter.format(member.getRegisterDateTime()));
}
}
}
스프링이 클래스를 검색해서 빈으로 등록해 주는 기능.
⇒ 설정 클래스에 빈을 등록하지 않아도 원하는 클래스를 빈으로 등록, 사용하는 것이 가능 ⇒ 설정과 관련된 코드가 감소
해당 클래스를 스캔 대상으로 표시
⇒ @Component 어노테이션에 값을 설정하지 않으면 클래스 이름의 첫 글자를 소문자로 바꾼 이름을 빈 이름으로 사용
// MemberDAO.java
package ex01;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
@Component ⇒ public MemberDAO memberDAO() { return new MemberDAO(); }
public class MemberDAO { ~~~~~~~~~ ~~~~~~~~~
private static long nextId = 0; 빈 타입 빈 이름
// MemberInfoPrinter.java
@Component("infoPrinter") // public MemberInfoPrinter infoPrinter() { return new MemberInfoPrinter(); }
public class MemberInfoPrinter {
// MemberListPrinter.java
@Component("listPrinter")
public class MemberListPrinter {
⇒ 설정 클래스에 @ComponentScan 어노테이션을 적용하면 @Component 어노테이션을 붙인 클래스를 스캔해서 스프링 빈으로 등록
// AppCtx.java
@Configuration
@ComponentScan(basePackages = { "ex01" })
public class AppCtx {
@Bean
@Qualifier("printer")
public MemberPrinter memberPrinter() {
return new MemberPrinter();
}
@Bean
@Qualifier("summaryPrinter")
public MemberSummaryPrinter memberPrinter2() {
return new MemberSummaryPrinter();
}
@Bean
public VersionPrinter versionPrinter() {
VersionPrinter versionPrinter = new VersionPrinter();
versionPrinter.setMajorVersion(5);
versionPrinter.setMinorVersion(3);
return versionPrinter;
}
}
@Component : 일반적인 빈
@Controller
@Service
@Repository
@Aspect
@Configuration
컴포넌트 스캔에 따른 충돌 처리
⇒ 오류 해결 방법 ⇒ 명시적으로 빈 이름을 설정
자동으로 등록되는 빈과 동일한 이름의 빈을 설정 클래스에서 등록하면 수동으로 등록한 빈이 사용됨
컴포넌트 스캔을 통해 자동으로 빈이 등록되도록 되어 있음
org.springframework.beans.factory.InitializingBean 인터페이스의 afterPropertiesSet() 메서드와
org.springframework.beans.factory.DisposableBean 인터페이스의 destroy() 메서드를 이용
@Bean 어노테이션의 initMethod 속성과 destroyMethod 속성을 사용해서 초기화 메서드와 소멸 메서드를 지정
별도로 스코프를 지정하지 않으면 기본적으로 싱글톤 스코프가 적용
@Scope("singleton") 또는 @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) 어노테이션을 사용해서 명시적으로 지정
새로운 인스터스가 요청될 때마다 생성되는 범위
@Scope("prototype") 또는 @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) 어노테이션을 사용
c1 == c2 : false
⇐ close() 메서드를 호출하지 않음
⇒ 프로토타입 범위를 갖는 빈은 완전한 라이프사이클을 따르지 않음
⇒ 빈 객체의 소멸 처리를 코드에서 직접해야 함
애플리케이션의 핵심 비즈니스 로직과 공통적인 기능(횡단 관심사, cross-cutting concerns)을 분리해서 모듈화하는 방법 .
로깅, 보안(인증, 인가), 트랜잭션 관리 등의 횡단 관심사를 비즈니스 로직과 분리해서 코드의 가독성, 유지보수성과 재활성을 높이는데 유용.

횡단관심사 : 비즈니스 로직과는 별도로 여러 모듈에 걸쳐 공통적으로 사용되는 기능.
예) 로깅, 보안, 트랜잭션 등
애스펙트(aspect) : 횡단 관심사를 모듈화한 것. 한 개 이상의 포인트 컷과 어드바이스의 조합으로 만들어짐.
조인포인트(join point) : 애스팩트가 적용될 수 있는 실행 지점. 메서드 호출, 객체 생성, 필드 접근 등이 될 수 있음. 스프링에서는 메서드 호출 단계만 가능
포인트 컷(pointcut) : 애스펙트가 적용 될 특정 조인포인트를 정의. 포인트컷은 조인포인트의 서브셋을 필터링하는 역할. 정규표현식이나 AspectJ의 문법을 이용해서 어떤 조인포인트를 사용할 것인지를 결정.
어드바이스(advice) : 포인트컷에서 정의한 특정 조인포인트에서 수행될 실제 작업을 의미. 어드바이스는 언제(조인포인트 전에, 후에, 예외 발생 시) 실행될지를 정의. 스프링에서 동작하는 시점에 따라 다섯 종류로 구분
위빙(weaving): 애스팩트를 실제 대상 객체에 적용하여 애스팩트와 비즈니스 로직을 결합하는 과정. 위빙은 컴파일 타임, 로드 타임, 런타임에 수행될 수 있음
스프링 AOP는 프록시 패턴을 사용해서 런타임에 위빙을 수행

스프링 프레임워크에서 AOP 기능을 사용하기 위해서는 spring-aop 모듈이 필요 ⇒ spring-context 모듈을 추가하면 자동으로 추가
⇒ 시간 계산 방식과 출력 방식에 코드 중복이 발생
⇒ 시간 계산 방식과 출력 방식에 변경이 필요하면 모든 중복 부분을 수정해야 함
⇒ 일관되게 변경이나 정책을 반영하는 것이 어려움
프록시 객체 ⇒ 핵심 기능을 구현하지 않는 대신 여러 객체에 공통적으로 적용할 수 있는 기능을 구현

Advice의 적용 순서는 설정 클래스에 빈 순서와 동일하게 동작 ⇒ 스프링 프레임워크와 자바 버전에 따라 상이
@Order 어너테이션을 사용해서 명시적으로 순서를 지정