[LG CNS AM Inspire CAMP 1기] Spring (2) - 어노테이션, 컴포넌트 스캔, 라이프사이클, AOP

니니지·2025년 1월 21일

LG CNS AM Inspire Camp 1기

목록 보기
26/47

INTRO

안녕하세요, 어제 작성하던 예제에서 다양한 어노테이션과 설정 방법을 알아보며 스프링에 대해 자세히 이해하는 시간을 가졌습니다.

1. @Import

함께 사용할 설정 클래스를 지정.

// 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);

2. getBean(bean_name, bean_type) 메서드

- 존재하지 않는 빈 이름을 사용했을 때


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

- 빈의 실제 타입과 지정한 타입이 다른 경우

- 빈 이름을 지정하지 않고 타입만으로 빈을 구할 수 있음

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

- 해당 타입의 빈 객체가 존재하지 않는 경우

- 같은 타입의 빈 객체가 두 개 이상 존재하는 경우

@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();
    }

3. @Autowired

사용 객체에서 의존 객체 필드에 @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 설정 클래스를 사용하도록 변경

// 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()));
        }
    }
}

4. @Qualifier

자동 주입 가능한 빈이 두 개 이상이면 자동 주입할 빈을 지정할 방법이 필요 ⇒ @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;

5. @Nullable

자동 주입할 빈이 존재하면 해당 빈을 전달하고, 존재하지 않으면 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()));
        }
    }
}

6. 컴포넌트 스캔

스프링이 클래스를 검색해서 빈으로 등록해 주는 기능.
⇒ 설정 클래스에 빈을 등록하지 않아도 원하는 클래스를 빈으로 등록, 사용하는 것이 가능 ⇒ 설정과 관련된 코드가 감소

- @Component

해당 클래스를 스캔 대상으로 표시
⇒ @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;    빈 타입   빈 이름	 

@Component 어노테이션에 값을 설정하면 해당 값을 빈 이름으로 사용

// MemberInfoPrinter.java 

@Component("infoPrinter") // public MemberInfoPrinter infoPrinter() { return new MemberInfoPrinter(); }
public class MemberInfoPrinter {


// MemberListPrinter.java

@Component("listPrinter")
public class MemberListPrinter {

@ComponentScan 어노테이션으로 스캔 설정

⇒ 설정 클래스에 @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
컴포넌트 스캔에 따른 충돌 처리
⇒ 오류 해결 방법 ⇒ 명시적으로 빈 이름을 설정
자동으로 등록되는 빈과 동일한 이름의 빈을 설정 클래스에서 등록하면 수동으로 등록한 빈이 사용됨

컴포넌트 스캔을 통해 자동으로 빈이 등록되도록 되어 있음

7. 스프링 빈 객체의 라이프사이클

- 객체 생성 → 의존 설정 → 초기화 → 소멸

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() 메서드를 호출하지 않음
⇒ 프로토타입 범위를 갖는 빈은 완전한 라이프사이클을 따르지 않음
⇒ 빈 객체의 소멸 처리를 코드에서 직접해야 함

8. AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)

애플리케이션의 핵심 비즈니스 로직과 공통적인 기능(횡단 관심사, cross-cutting concerns)을 분리해서 모듈화하는 방법 .

로깅, 보안(인증, 인가), 트랜잭션 관리 등의 횡단 관심사를 비즈니스 로직과 분리해서 코드의 가독성, 유지보수성과 재활성을 높이는데 유용.

- 주요 개념

횡단관심사 : 비즈니스 로직과는 별도로 여러 모듈에 걸쳐 공통적으로 사용되는 기능.
예) 로깅, 보안, 트랜잭션 등

애스펙트(aspect) : 횡단 관심사를 모듈화한 것. 한 개 이상의 포인트 컷과 어드바이스의 조합으로 만들어짐.

조인포인트(join point) : 애스팩트가 적용될 수 있는 실행 지점. 메서드 호출, 객체 생성, 필드 접근 등이 될 수 있음. 스프링에서는 메서드 호출 단계만 가능

포인트 컷(pointcut) : 애스펙트가 적용 될 특정 조인포인트를 정의. 포인트컷은 조인포인트의 서브셋을 필터링하는 역할. 정규표현식이나 AspectJ의 문법을 이용해서 어떤 조인포인트를 사용할 것인지를 결정.

어드바이스(advice) : 포인트컷에서 정의한 특정 조인포인트에서 수행될 실제 작업을 의미. 어드바이스는 언제(조인포인트 전에, 후에, 예외 발생 시) 실행될지를 정의. 스프링에서 동작하는 시점에 따라 다섯 종류로 구분

위빙(weaving): 애스팩트를 실제 대상 객체에 적용하여 애스팩트와 비즈니스 로직을 결합하는 과정. 위빙은 컴파일 타임, 로드 타임, 런타임에 수행될 수 있음

스프링 AOP는 프록시 패턴을 사용해서 런타임에 위빙을 수행


스프링 프레임워크에서 AOP 기능을 사용하기 위해서는 spring-aop 모듈이 필요 ⇒ spring-context 모듈을 추가하면 자동으로 추가

⇒ 시간 계산 방식과 출력 방식에 코드 중복이 발생
⇒ 시간 계산 방식과 출력 방식에 변경이 필요하면 모든 중복 부분을 수정해야 함
⇒ 일관되게 변경이나 정책을 반영하는 것이 어려움

프록시 객체 ⇒ 핵심 기능을 구현하지 않는 대신 여러 객체에 공통적으로 적용할 수 있는 기능을 구현

Advice의 적용 순서는 설정 클래스에 빈 순서와 동일하게 동작 ⇒ 스프링 프레임워크와 자바 버전에 따라 상이

@Order 어너테이션을 사용해서 명시적으로 순서를 지정

profile
지니니

0개의 댓글