[LG CNS AM CAMP 1기] 백엔드 II 2 | Spring

letthem·2025년 1월 21일
1

LG CNS AM CAMP 1기

목록 보기
18/31
post-thumbnail

@Import

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

ex 1)

AppConf1.java

package com.test.test1.config;

import com.test.test1.MemberDAO;
import com.test.test1.MemberPrinter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

// 의존 관계가 없는것들
@Configuration
@Import(AppConf2.class)
public class AppConf1 {
    @Bean
    public MemberDAO memberDAO() {
        return new MemberDAO();
    }

    @Bean
    public MemberPrinter memberPrinter() {
        return new MemberPrinter();
    }
}

MainForSpring.java

// ctx = new AnnotationConfigApplicationContext(AppCtx.class);
//ctx = new AnnotationConfigApplicationContext(AppConf1.class, AppConf2.class);
ctx = new AnnotationConfigApplicationContext(AppConf1.class);

ex 2) @Import 어노테이션에 두 개 이상의 설정 클래스를 지정하는 경우

AppConf1.java

@Configuration
@Import({ AppConf2.class, AppConf3.class })
public class AppConf1 {
    ... (생략) ... 

=> 중괄호로 나열 !


getBean("bean_name", bean_type) 메서드

ex 1) 존재하지 않는 빈 이름을 사용했을 때

private static void processVersionCommand(String[] args) {
    // VersionPrinter versionPrinter = ctx.getBean("versionPrinter", VersionPrinter.class);
    VersionPrinter versionPrinter = ctx.getBean("notexistent", VersionPrinter.class);
    versionPrinter.print();
}

"notexistent" 존재하지 않는 빈 이름을 사용했을 때 !

🚨 error. NoSuchBeanDefinitionException: No bean named 'notexistent' available

ex 2) 빈의 실제 타입과 지정한 타입이 다른 경우

private static void processVersionCommand(String[] args) {
    // VersionPrinter versionPrinter = ctx.getBean("versionPrinter", VersionPrinter.class);
    // VersionPrinter versionPrinter = ctx.getBean("notexistent", VersionPrinter.class);
    VersionPrinter versionPrinter = ctx.getBean("versionPrinter", MemberInfoPrinter.class);
    versionPrinter.print();
}

🚨 Type mismatch: cannot convert from MemberInfoPrinter to VersionPrinter

ex 3-1) 빈 이름을 지정하지 않고 타입만으로 빈을 구할 수 있다

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

private static void processVersionCommand(String[] args) {
    // VersionPrinter versionPrinter = ctx.getBean("versionPrinter", VersionPrinter.class);
    // VersionPrinter versionPrinter = ctx.getBean("notexistent", VersionPrinter.class);
    // VersionPrinter versionPrinter = ctx.getBean("versionPrinter", MemberInfoPrinter.class);
    VersionPrinter versionPrinter = ctx.getBean(VersionPrinter.class);
    versionPrinter.print();
}

ctx.getBean(VersionPrinter.class); 이렇게로만 가져올 수 있다 !

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

private static void processVersionCommand(String[] args) {
    // VersionPrinter versionPrinter = ctx.getBean("versionPrinter", VersionPrinter.class);
    // VersionPrinter versionPrinter = ctx.getBean("notexistent", VersionPrinter.class);
    // VersionPrinter versionPrinter = ctx.getBean("versionPrinter", MemberInfoPrinter.class);
    VersionPrinter versionPrinter = ctx.getBean(NotExistent.class);
    versionPrinter.print();
}

ctx.getBean(NotExistent.class); 로 없는 경우

🚨 NotExistent cannot be resolved to a type

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

AppConf2.java

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

같은 타입 2개 생성

MainForSpring.java

private static void processVersionCommand(String[] args) {
    VersionPrinter versionPrinter = ctx.getBean(VersionPrinter.class);
    versionPrinter.print();
}

VersionPrinter 라는 같은 타입 !!

🚨 NoUniqueBeanDefinitionException: No qualifying bean of type 'ex01.VersionPrinter' available: expected single matching bean but found 2: versionPrinter,newVersionPrinter

VersionPrinter 타입을 한정지을 수 없다. 내가 어떤 걸 리턴해줘야하는지 모르겠다.


@Autowired

사용 객체에서 의존 객체 필드에 @Autowired 어노테이션을 붙이면 스프링이 자동으로 해당 타입의 빈 객체를 찾아서 필드에 할당해준다.

예시 1)

ChangePasswordService.java

public class ChangePasswordService {
    @Autowired
    private MemberDAO memberDAO; // 자동으로 인스턴스에 넣어주므로

    /* 별도로 의존 주입을 위한 코드가 필요없어진다.
    public ChangePasswordService(MemberDAO memberDAO) {
        this.memberDAO = memberDAO;
    }
    */
    ...(생략)...

@Autowired 어노테이션을 setter 메서드에 붙이면, 스프링이 해당 setter 메서드를 통해 의존성을 주입 (= setter 기반 의존성 주입)

예시 2)

MemberInfoPrinter.java

package com.test.test1;

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

전제조건 : 해당하는 객체들이 Bean으로 등록되어 있어야 한다.

AppCtx 설정 클래스를 사용하도록 변경 ⬇️
AppCtx.java

@Bean
public ChangePasswordService changePwdSvc() {
    //return new ChangePasswordService(memberDAO);
    return new ChangePasswordService();
}

MainForSpring.java

ctx = new AnnotationConfigApplicationContext(AppCtx.class);

예시 3) 일치하는 빈이 없는 경우

MemberListPrinter.java

package com.test.test1;

import org.springframework.beans.factory.annotation.Autowired;

import java.util.Collection;

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

🚨 No qualifying bean of type 'ex01.MemberDAO' available: expected at least 1 bean which qualifies as autowire candidate.

MemberDAO가 빈으로 등록되어 있는지 먼저 확인해야 한다.

예시 4) 일치하는 빈이 두 개 이상인 경우

AppCtx.java

@Bean
public MemberPrinter memberPrinter() {
    return new MemberPrinter();
}

@Bean
public MemberPrinter memberPrinter2() {
    return new MemberPrinter();
}

2개의 빈이 MemberPrinter로 타입이 같다

MemberListPrinter.java

public class MemberListPrinter {
  
    @Autowired
    private MemberPrinter printer;
    
    ...(생략)...

어떤 것을 넣어야할지 모호해져서 error 발생

`🚨 .NoUniqueBeanDefinitionException: No qualifying bean of type 'ex01.MemberPrinter' available: expected single matching bean but found 2: memberPrinter2,memberPrinter

`

@Qualifier

자동 주입 가능한 빈이 두 개 이상이면 자동 주입할 빈을 지정할 방법이 필요하다.
@Qualifier를 이용해서 자동 주입 대상 빈을 한정지을 수 있다.

예시 1)

AppCtx.java

@Bean
@Qualifier("printer")
public MemberPrinter memberPrinter() {
    return new MemberPrinter();
}

MemberListPrinter.java

@Autowired
@Qualifier("printer")
private MemberPrinter printer;

MemberInfoPrinter.java

@Autowired
@Qualifier("printer")
public void setMemberPrinter(MemberPrinter printer) {
    this.printer = printer;
}

css class 선택자와 비슷한 느낌..!!

  • 빈 설정에 @Qualifier 를 사용하지 않으면 빈 이름이 기본 한정자로 지정된다!
@Bean
public MemberPrinter memberPrinter2() {	<= memberPrinter2 = 빈 이름 = 기본 한정자
    return new MemberPrinter();
}
  • @Autowired(빈을 사용하는 쪽)도 @Qualifier가 없으면 필드나 파라미터 이름을 한정자로 사용한다.
@Autowired
    private MemberPrinter printer;	<= printer = 필드 이름 = 기본 한정자

상속받으면서 발생하는 오류 해결 방법들

MemberSummaryPrinter.java

MemberPrinter를 상속받음 !

package com.test.test1;

public class MemberSummaryPrinter extends MemberPrinter {
    @Override
    public void print(Member member) {
        System.out.printf("회원정보: 이메일=%s, 이름=%s\n", member.getEmail(), member.getName());
    }
}

AppCtx 설정 클래스에 MemberSummaryPrinter를 빈으로 등록

@Bean
@Qualifier("printer")
public MemberPrinter memberPrinter() {
    return new MemberPrinter();
}

@Bean
public MemberSummaryPrinter memberPrinter2() {
    return new MemberSummaryPrinter();
}

MemberListPrinter.java

@Qualifier 제거

package com.test.test1;

import org.springframework.beans.factory.annotation.Autowired;

import java.util.Collection;

public class MemberListPrinter {
    @Autowired
    private MemberDAO memberDAO;
    @Autowired
    //@Qualifier("printer")
    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));
    }
}

실행하면 오류가 난다.
🚨 NoUniqueBeanDefinitionException: No qualifying bean of type 'ex01.MemberPrinter' available: expected single matching bean but found 2: memberPrinter2,memberPrinter
왤까? 상속 받았기 때문에 상속관계에 있는 애들이 다 나온다..ㅠㅠ

해결방법 1) @Qualifier 이용해서 주입할 빈을 한정

AppCtx.java

@Bean
@Qualifier("summaryPrinter")
public MemberSummaryPrinter memberPrinter2() {
    return new MemberSummaryPrinter();
}

MemberListPrinter.java

@Autowired
@Qualifier("summaryPrinter")
private MemberPrinter printer;

해결방법 2) 자식 클래스 타입의 빈을 자동 주입

AppCtx.java
@Qualifier 제거

@Bean
@Qualifier("printer")
public MemberPrinter memberPrinter() {
    return new MemberPrinter();
}
    
@Bean
// @Qualifier("summaryPrinter")
public MemberSummaryPrinter memberPrinter2() {
    return new MemberSummaryPrinter();
}

MemberListPrinter.java
1. @Qualifier 제거
2. private MemberSummaryPrinter printer;
사용 클래스에서 자식 클래스 타입으로 명확하게 주입 !

public class MemberListPrinter {
    @Autowired
    private MemberDAO memberDAO;
    
    @Autowired
    // @Qualifier("summaryPrinter")
    private MemberSummaryPrinter printer;

자동 주입할 대상이 필수가 아닌 경우 → 선택적으로 주입

해결방법 1) @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()));
        }
    }
}

해결방법 2-1) 의존 주입 대상에 자바 8의 Optional 객체를 사용

MemberPrinter.java

public class MemberPrinter {
    // @Autowired(required = false)
    private DateTimeFormatter dateTimeFormatter;

    // @Autowired(required = false)
    @Autowired
    public void setDateTimeFormatter(Optional<DateTimeFormatter> dateTimeFormatterOptional) {
        if (dateTimeFormatterOptional.isPresent()) { // 값이 오는 경우
            this.dateTimeFormatter = dateTimeFormatter;
        } else { // 값이 오지 않는 경우
            this.dateTimeFormatter = null;
        }
    }

해결방법 2-2) 필드에 Optional로 구현

public class MemberPrinter {
    // @Autowired(required = false)
    @Autowired
    private Optional<DateTimeFormatter> dateTimeFormatterOptional;
    
    // @Autowired(required = false)
//    @Autowired
//    public void setDateTimeFormatter(Optional<DateTimeFormatter> dateTimeFormatterOptional) {
//        if (dateTimeFormatterOptional.isPresent()) {
//            this.dateTimeFormatter = dateTimeFormatterOptional.get();
//        } else {
//            this.dateTimeFormatter = null;    
//        }
//    }
    
    public void print(Member member) {
        DateTimeFormatter dateTimeFormatter = dateTimeFormatterOptional.orElse(null);
        
        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()));
        }
    }
}

해결방법 3) 자동 주입할 빈이 존재하면 해당 빈을 전달하고, 존재하지 않으면 null을 전달 => @Nullable

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

or

setter 메서드에 붙일 수도 있다.

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

자동주입할 빈이 없다면 ❓

  • setter 메서드에 required = false로 되어있는 것은 setter 메서드를 호출하지 않는 반면
  • setter 메서드에 @Nullable 로 되어 있으면 setter 메서드를 반드시 호출한다.

AppCtx.java

@Bean
public MemberPrinter memberPrinter() {
    return new MemberPrinter();
}

@Bean
public MemberSummaryPrinter memberPrinter2() {
    return new MemberSummaryPrinter();
}

@Bean
public MemberInfoPrinter memberInfoPrinter() {
    MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
    infoPrinter.setMemberPrinter(memberPrinter2());
    return infoPrinter;
}

MemberInfoPrinte.java

public class MemberInfoPrinter {
    private MemberDAO memberDAO;
    private MemberPrinter printer;
    
    @Autowired
    public void setMemberDAO(MemberDAO memberDAO) {
        this.memberDAO = memberDAO;
    }
    
    @Autowired						⇐ 사용하지 않으면 MemberSummaryPrinter 타입을 주입
							⇐ 사용하면 MemberPrinter 타입을 주입 → 예외 발생
    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();
    }
}

컴포넌트 스캔

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

@Component

해당 클래스를 스캔 대상으로 표시
=> @Component 어노테이션에 값을 설정하지 않으면 클래스 이름의 첫 글자를 소문자로 바꾼 이름을 빈 이름으로 사용

MemberDAO.java

package com.test.test1;

import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

@Component // => public MemberDAO memberDAO() { return new MemberDAO(); }
public class MemberDAO {
    private static long nextId = 0;

    private Map<String, Member> map = new HashMap<>();

    public Member selectByEmail(String email) {
        return map.get(email);
    }

    public void insert(Member member) {
        member.setId(++nextId);
        map.put(member.getEmail(), member);
    }

    public void update(Member member) {
        map.put(member.getEmail(), member);
    }

    public Collection<Member> selectAll() {
        return map.values();
    }
}
@Component // public MemberDAO memberDAO() { return new MemberDAO(); 
                     ~~~~~~~~~ ~~~~~~~~~ 
                     빈 타입     빈 이름

ChangePasswordService.java

@Component
public class ChangePasswordService {

MemberRegisterService.java

@Component
public class MemberRegisterService {

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

MemberInfoPrinter.java

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

MemberListPrinter.java

@Component("listPrinter")
public class MemberListPrinter {

@ComponentScan

@ComponentScan 어노테이션으로 스캔 설정
=> 설정 클래스에 @ComponentScan 어노테이션을 적용하면 @Component 어노테이션을 붙인 클래스를 스캔해서 스프링 빈으로 자동 등록 !!

AppCtx.java
=> @Component 어노테이션을 사용한 클래스를 빈으로 등록하는 코드를 삭제

package com.test.test1.config;

import com.test.test1.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

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

빈 등록해주던 메서드는 다 빠졌다 !

MainForSpring.java
빈의 이름이 바뀌었기 때문에 변경된 빈 이름으로 사용

private static void processInfoCommand(String[] args) {
    if (args.length != 2) {
        printHelp();
        return;
    }

    MemberInfoPrinter memberInfoPrinter = ctx.getBean("infoPrinter", MemberInfoPrinter.class);
    memberInfoPrinter.printMemberInfo(args[1]);
}

private static void processListCommand(String[] args) {
    MemberListPrinter memberListPrinter = ctx.getBean("listPrinter", MemberListPrinter.class);
    memberListPrinter.printAll();
}
private static void processNewCommand(String[] args) {
    if (args.length != 5) {
        printHelp();
        return;
    }

    // MemberRegisterService regSvc = assembler.getRegSvc();
    MemberRegisterService regSvc = ctx.getBean(MemberRegisterService.class);
    RegisterRequest reg = new RegisterRequest();
    reg.setEmail(args[1]);
    reg.setName(args[2]);
    reg.setPassword(args[3]);
    reg.setConfirmPassword(args[4]);

    if (!reg.isPasswordEqualToConfirmPassword()) {
        System.out.println("패스워드와 패스워드 확인이 일치하지 않습니다.");
        return;
    }

    try {
        regSvc.regist(reg);
        System.out.println("등록되었습니다.");
    } catch(DuplicateMemberException e) {
        System.out.println("이미 존재하는 이메일입니다.");
    }
}

private static void processChangeCommand(String[] args) {
    if (args.length != 4) {
        printHelp();
        return;
    }

    // ChangePasswordService pwdSvc = assembler.getPwdSvc();
    ChangePasswordService pwdSvc = ctx.getBean(ChangePasswordService.class);
    try {
        pwdSvc.changePassword(args[1], args[2], args[3]);
        System.out.println("패스워드를 변경하였습니다.");
    } catch(RuntimeException e) {
        System.out.println(e.getMessage());
    }
}

기본 스캔 대상

@Component

일반적인 Bean

@Controller

스프링 MVC에서 사용자의 요청을 받고 응답을 반환하는 역할

@Service

비즈니스 로직 처리

@Repository

데이터 접근 계층. JPA를 이용해서 구현할 때 DB access 로직을 담는 곳

@Aspect

AOP(Aspect Oriented Programming). 공통 관심사를 로직에서 분리 (로그인, 트랜잭션 관리 등) => 중복 발생을 최소화

@Configuration

설정 파일 정의


컴포넌트 스캔에 따른 충돌 처리

ex02 패키지를 만들고 MemberRegisterService 만들기 ⬇️

package com.test.test1.ex02;

import org.springframework.stereotype.Component;

@Component
public class MemberRegisterService { // memberRegisterService 이름의 빈이 등록
}

AppCtx.java

@Configuration
@ComponentScan(basePackages = { "ex01", "ex02" })
public class AppCtx {

두 개의 빈이 충돌난다. MemberRegisterService가 ex01, ex02 에 다 있어서 자동 스캔하다보니 다른 패키지의 같은 이름의 클래스를 발견해서 오류를 낸다.
=> 오류 해결 방법 => 명시적으로 빈 이름을 설정

@Component("ex02MemberRegisterService")
public class MemberRegisterService { // memberRegisterService 이름의 빈이 등록
}

컴포넌트 이름을 이렇게 부여하면 해결된다 !!

자동으로 등록되는 빈과 동일한 이름의 빈을 설정 클래스에서 등록하면 수동으로 등록한 빈이 사용됨

MemberDAO.java
=> 컴포넌트 스캔을 통해 자동으로 빈 등록

@Component
public class MemberDAO {

AppCtx.java
=> 수동으로 빈을 등록하는 코드를 추가

@Configuration
@ComponentScan(basePackages = { "ex01", "ex02" })
public class AppCtx {
    @Bean
    public MemberDAO memberDAO() {
        return new MemberDAO();
    }

아무 문제 없다 -! 자동 & 수동 모두 등록되면 어떤 걸 먼저 볼까? => 수동


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

=> 객체 생성 -> 의존 설정 -> 초기화 -> 소멸
                        ~~~~~~~~~~~~

방법 1) org.springframework.beans.factory.InitializingBean 인터페이스의 afterPropertiesSet() 메서드와

org.springframework.beans.factory.DisposableBean 인터페이스의 destroy() 메서드를 이용

package com.test.test1.ex03;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class Client implements InitializingBean, DisposableBean {
    
    private String host; 
    
    public void setHost(String host) {
        this.host = host;
    }
    
    public void send() {
        System.out.println("Client.send() is called ... " + this.host);
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        // 초기 설정 작업들
        System.out.println("Client.afterPropertiesSet() is called ... ");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("Client.destroy() is called ... ");
    }
}

각각의 추상메서드를 override해서 구현해주었다.

스프링 설정 클래스(AppCtx.java)에 Clent를 빈으로 등록

@Configuration
@ComponentScan(basePackages = { "ex01", "ex02" })
public class AppCtx {
    @Bean
    public Client client() {
        Client client = new Client();
        client.setHost("www.test.com");
        return client;
    }

실행 클래스(MainForClient.java)를 만들어서 Client 빈의 send() 메서드를 실행

package com.test.test1.main;

import com.test.test1.config.AppCtx;
import com.test.test1.ex03.Client;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;

public class MainForClient {
    public static void main(String[] args) {
        AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);

        Client c = ctx.getBean(Client.class);
        c.send();

        System.out.println("before ctx.close()");
        ctx.close();
        System.out.println("after ctx.close()");
    }
}

다음과 같이 실행된다.

Client.afterPropertiesSet() is called ... 
Client.send() is called ... www.test.com
before ctx.close()
Client.destroy() is called ... 
after ctx.close()

방법 2) @Bean 어노테이션의 initMethod 속성과 destroyMethod 속성을 사용해서 초기화 메서드와 소멸 메서드를 지정

Client2 클래스

package com.test.test1.ex03;

public class Client2 {
    private String host;

    public void setHost(String host) {
        this.host = host;
    }

    public void send() {
        System.out.println("Client.send() is called ... " + this.host);
    }

    public void connect() {
        System.out.println("Client.connect() is called ... ");
    }

    public void close() {
        System.out.println("Client.close() is called ... ");
    }
}

스프링 설정 클래스(AppCtx.java)에 Client2 빈을 등록

@Configuration
@ComponentScan(basePackages = { "ex01", "ex02" })
public class AppCtx {
    @Bean(initMethod = "connect", destroyMethod = "close")
    public Client2 client2() {
        Client2 client = new Client2();
        client.setHost("www.test.com");
        return client;
    }

    // @Bean
    public Client client() {
        Client client = new Client();
        client.setHost("www.test.com");
        return client;
    }

실행 클래스(MainForClent.java)에 Client2를 사용하도록 수정

public class MainForClient {
    public static void main(String[] args) {
        AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);

        Client2 c = ctx.getBean(Client2.class);
        c.send();

        System.out.println("before ctx.close()");
        ctx.close();
        System.out.println("after ctx.close()");
    }
}

실행결과

Client.connect() is called ... 
Client.send() is called ... www.test.com
before ctx.close()
Client.close() is called ... 
after ctx.close()

빈의 스코프(범위)를 정의해서 빈이 생성되고 관리되는 방식을 제어할 수 있다

싱글톤 범위

별도로 스코프를 지정하지 않으면 기본적으로 싱글톤 스코프가 적용

  • @Scope("singleton")
  • @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)

AppCtx.java

@Configuration
@ComponentScan(basePackages = { "ex01", "ex02" })
public class AppCtx {
    @Bean(initMethod = "connect", destroyMethod = "close")
    @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) <=singleton
    public Client2 client2() {
        Client2 client = new Client2();
        client.setHost("www.test.com");
        return client;
    }

MainForClient.java

public class MainForClient {
    public static void main(String[] args) {
        AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);

        Client2 c1 = ctx.getBean(Client2.class);
        Client2 c2 = ctx.getBean(Client2.class);

        System.out.println("c1 == c2 : " + (c1 == c2)); // true
        ctx.close();
    }
}

실행 결과

Client.connect() is called ... 
c1 == c2 : true
Client.close() is called ... 

true로 뜬다. => 싱글톤 스코프이므로 인스턴스가 하나만 생성된다!
close도 자동으로 된다. 😇

프로토타입 범위

새로운 인스턴스가 요청될 때마다 생성되는 범위

  • @Scope("prototype")
  • @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

AppCtx.java

@Configuration
@ComponentScan(basePackages = { "ex01", "ex02" })
public class AppCtx {
    @Bean(initMethod = "connect", destroyMethod = "close")
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) <= prototype
    public Client2 client2() {
        Client2 client = new Client2();
        client.setHost("www.test.com");
        return client;
    }

MainForClient.java

public class MainForClient {

    public static void main(String[] args) {
        AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
        
        Client2 c1 = ctx.getBean(Client2.class);
        Client2 c2 = ctx.getBean(Client2.class);
        
        System.out.println("c1 == c2 : " + (c1 == c2)); // false
        
        ctx.close();
    }
}

실행 결과

Client.connect() is called ... 
Client.connect() is called ... 
c1 == c2 : false

false로 뜬다. => 프로토타입 스코프이므로 인스턴스가 따로따로 생성되므로 커넥션이 2번 일어난다.

또한 close() 메서드를 호출하지 않는다.
=> 프로토타입 범위를 갖는 빈은 완전한 lifecycle을 따르지 않는다.
=> 빈 객체의 소멸 처리를 코드에서 직접해야 한다 🥵


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

  • 애플리케이션의 핵심 비즈니스 로직과 공통적인 기능(횡단 관심사, cross-cutting concerns)을 분리해서 모듈화하는 방법
  • 로깅, 보안(인증, 인가), 트랜잭션 관리 등의 횡단 관심사를 비즈니스 로직과 분리해서 코드의 가독성, 유지보수성과 재활성을 높이는데 유용

똑같은 코드가 여러 메서드에 분산되어 중복으로 사용되고 있다,, 🥵
관심사를 분리하자 !!!

주요 개념

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

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

어드바이스어노테이션사용 시
Before advice@Before대상 메서드가 실행되기 전에 적용할 어드바이스를 정의
After returning advice@AfterReturning대상 메서드가 성공적으로 실행되고 결과값을 반환한 후 적용할 어드바이스를 정의
After throwing advice@AfterThrowing대상 메서드에서 예외가 발생했을 때 적용할 어드바이스를 정의
After advice@After대상 메서드의 정상 수행 여부와 상관없이 무조건 실행되는 어드바이스를 정의
Around advice@Around대상 메서드의 호출 전후, 예외 발생 등 모든 시점에 적용할 수 있는 어드바이스를 정의

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

스프링 없이 AOP 구현 ⬇️

Calculator 인터페이스를 정의

package com.test.test1.ex04;

public interface Calculator {
    public long factorial(long num);
}

Calculator 인터페이스를 상속하는 ImpCalculator, RecCalculator 클래스를 정의

ImpCalculator

package com.test.test1.ex04;

public class ImpCalculator implements Calculator{
    @Override
    public long factorial(long num) {
        long result = 1;
        for (long i = 1; i <= num; i++) {
            result *= i;
        }
        return result;
    }
}

RecCalculator

package com.test.test1.ex04;

public class RecCalculator implements Calculator{
    @Override
    public long factorial(long num) {
        if (num == 1)
            return 1;

        // 재귀 호출
        return num * factorial(num - 1);
    }
}

실행 시간 출력 기능을 추가 ⇒ 메서드의 시작과 끝에서 시간을 구하고 차이를 출력

ImpCalculator

package com.test.test1.ex04;

public class ImpCalculator implements Calculator{
    @Override
    public long factorial(long num) {
        long start = System.nanoTime();

        long result = 1;
        for (long i = 1; i <= num; i++) {
            result *= i;
        }

        long end = System.nanoTime();
        System.out.printf("ImpCalculator.factorial(%d) 실행시간 = %d\n", num, (end-start));

        return result;
    }

    public static void main(String[] args) {
        ImpCalculator c = new ImpCalculator();
        c.factorial(100000000);
    }
}

RecCalculator 클래스에 factorial 메서드 적용이 불가 => 재귀 호출 방식이므로 시작과 끝 시간을 측정하는 것이 불가

실행 클래스에서 메서드 실행 전후에 값을 계산하는 방식으로 변경

MainForCalculator

package com.test.test1.ex04;

public class MainForCalculator {
    public static void main(String[] args) {
        
    }
}

ImplCalculator에 추가했던 코드를 삭제

ImplCalculator

package com.test.test1.ex04;

public class ImpCalculator implements Calculator {
    @Override
    public long factorial(long num) {
        long result = 1;
        for (long i = 1; i <= num; i ++) {
            result *= i;
        }
        return result;
    }
}

MainForCalculator.java

package com.test.test1.ex04;

public class MainForCalculator {
    public static void main(String[] args) {
        final long num = 10L;

        ImpCalculator imp = new ImpCalculator();
        long start1 = System.nanoTime();
        imp.factorial(num);
        long end1 = System.nanoTime();
        System.out.printf("ImpCalculator.factorial(%d) 실행시 = $d\n", num, (end1-start1));

        RecCalculator rec = new RecCalculator();
        long start2 = System.nanoTime();
        rec.factorial(num);
        long end2 = System.nanoTime();
        System.out.printf("RecCalculator.factorial(%d) 실행시 = $d\n", num, (end2-start2));
    }
}

=> 시간 계산 방식과 출력 방식에 코드 중복이 발생
=> 시간 계산 방식과 출력 방식에 변경이 필요하면 모든 중복 부분을 수정해야 한다. 🥵
=> 일관되게 변경이나 정책을 반영하는 것이 어렵다 ㅠ.ㅠ

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

공통의 인터페이스를 상속받아야 하고
필드를 만들고 생성자도 있어야 한다.

ExeTimeCalculator.java <- 프록시 객체 🩷

package com.test.test1.ex04;

public class ExeTimeCalculator implements Calculator {
    private Calculator delegate; // 인터페이스를 필드로 갖고 온다. rec, imp 가 들어올 것

    public ExeTimeCalculator(Calculator delegate) {
        this.delegate = delegate; // 생성자에서 원하는 인스턴스를 넣어 캐스팅이 가능
    }

    @Override
    public long factorial(long num) {
        long result = delegate.factorial(num);
        return result;
    }
}

MainForCalculator.java

package com.test.test1.ex04;

public class MainForCalculator {
    public static void main(String[] args) {
        final long num = 10L;

        Calculator imp = new ExeTimeCalculator(new ImpCalculator());
        System.out.println(imp.factorial(num));

        Calculator rec = new ExeTimeCalculator(new RecCalculator());
        System.out.println(rec.factorial(num));
    }
}

프록시 객체에 공통 기능(실행 시간 계산 및 출력)을 추가

ExeTimeCalculator.java

public class ExeTimeCalculator implements Calculator {
    private Calculator delegate;
    
    public ExeTimeCalculator(Calculator delegate) {
        this.delegate = delegate;
    }

    @Override
    public long factorial(long num) {
        long start = System.nanoTime();
        long result = delegate.factorial(num);
        long end = System.nanoTime();
        
        System.out.printf("%s.factorial(%d) 실행시간 = %d\n", 
            this.delegate.getClass().getSimpleName(), num, (end - start));
        return result;
    }
}


디자인 패턴 입문 책 추천 ⬇️

디자인 패턴 책 추천 ⬇️

보안 책 추천 ⬇️


스프링 AOP 구현

  1. Aspect를 사용할 클래스에 @Aspect 를 추가
  2. @Pointcut 으로 공통 기능을 적용할 Pointcut을 정의
  3. 공통 기능을 구현한 메서드에 @Around 를 적용

aspects 의존성 추가

Aspect 정의 !

ExeTimeAspect.java

package com.test.test1.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import java.util.Arrays;

@Aspect
public class ExeTimeAspect {
    // 공통 기능을 적용할 대상을 설정
    // ex04 패키지와 그 하위 패키지에 위치한 public 메서드를 Pointcut으로 설정
    @Pointcut("execution(public * ex04..*(..))")
    private void publicTarget() {

    }

    // Around Advice
    // publicTarget() 메서드에 정의한 Pointcut에 공통 기능을 적용
    // measure => 사용자 정의 메서드 => 공통 기능을 구현
    // ProceedingJoinPoint => 프록시 대상 객체의 메서드를 호출할 때 사용
    @Around("publicTarget(")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.nanoTime();
        
        try{
            Object result = joinPoint.proceed(); // proceed() 는 원래 실행되어야 할 메서드가 실행되는 것 !

            return result;
        } finally {
            long end = System.nanoTime();
            
            System.out.printf("%s.%s(%s) 실행결과 = %d \n",
                    joinPoint.getTarget().getClass().getSimpleName(),
                    joinPoint.getSignature().getName(),
                    Arrays.toString(joinPoint.getArgs()),
                    (end - start)
            );
        }
        
        
    }
}

스프링 설정 클래스에 @EnableAspectJAutoProxy 어노테이션을 추가

config/AppCtxAspect.java

package com.test.test1.config;

import com.test.test1.aop.ExeTimeAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public class AppCtxAspect {
    @Bean
    public ExeTimeAspect exeTimeAspect() {
        return new ExeTimeAspect();
    }

    @Bean
    public Calculator recCalculator() {
        return new RecCalculator();
    }

    @Bean
    public Calculator impCalculator() {
        return new ImpCalculator();
    }
}

스프링 컨테이너를 생성

MainForAspect.java

package com.test.test1.main;

import com.test.test1.config.AppCtxAspect;
import com.test.test1.ex04.Calculator;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;

public class MainForAspect {

    public static void main(String[] args) {
        AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtxAspect.class);

        Calculator imp = ctx.getBean("impCalculator", Calculator.class);
        System.out.println(imp.factorial(10));

        Calculator rec = ctx.getBean("recCalculator", Calculator.class);
        System.out.println(rec.factorial(10));

        ctx.close();
    }

}

0개의 댓글

관련 채용 정보