잡동사니

스프링 추전 강좌, 스프링 프레임워크 핵심 기술 내용 정리 및 후기 본문

IT/Spring

스프링 추전 강좌, 스프링 프레임워크 핵심 기술 내용 정리 및 후기

yeTi 2019. 12. 20. 11:09

인프런의 스프링 프레임워크 핵심 기술 - 백기선을 수강하면서 내용을 정리하고 간단한 후기를 남깁니다.

스프링 프레임워크에 대한 전체적인 개념을 알고 싶으시면 Spring Framework 개념 이해하기를 확인해보세요.

IoC 컨테이너와 빈

IoC란

Inversion of Control의 약어로 의존 관계 주입(Dependency Injection) 이라고도 하며 어떤 객체를 사용할 때 직접 만들어서 사용하는 것이 아니라 주입받아 사용하는 것을 말한다.

직접 사용의 예

BookRepository repository = new BookRepository();
BookService service = new BookService(repository);

IoC의 예

@Autowired
BookRepository repository;

BookService service = new BookService(repository)

IoC 컨테이너

IoC를 제공하는 컨테이너로 리소스에서 빈 정보를 읽어 빈을 생성하고 제공하는 중앙 저장소이다.

최상위 인터페이스로 BeanFactory가 있고 구현체로 ApplicationContext를 사용한다.

ApplicationContext

ApplicationContext는 IoC 컨테이너의 구현체로 빈을 등록하고 IoC을 수행한다.

빈을 등록하는 다양한 방법이 있는데 나열하면 다음과 같다.

  • application.xml 활용
    • bean 태그로 로 직접 등록 및 의존성 주입
    • component-scan 활용
    • ClassPathXmlApplicationContext에 설정 등록
  • Java 활용 (@Configuration)
    • @Bean을 활용하여 직접 등록 및 의존성 주입
    • @ComponentScan 활용
    • AnnotationConfigApplicationContext에 살정 등록
  • @SpringBootApplucation
    • 내부에 @ComponentScan@Configuration을 가짐

빈을 등록할 때 Scope을 지정할 수 있다.

  • Prototype : 매번 생성
    • Request : Request마다 생성
    • Session : 세션마다 생성
    • ...
  • Singletone : 싱글톤으로 생성 (Default)

@Autowired

필요한 의존 객체의 타입에 해당하는 빈을 찾아 주입한다. 이름으로 찾아주기도 하는데 사용은 비추다.

사용할 수 있는 곳은 다음과 같다.

  • 생성자
  • Setter
  • Field

동일한 구현체가 두개 이상 있을 때는 다음과 같이 조치해줘야 한다.

  • @Primary - 추천
  • 리스트로 객체를 생성
  • @Qualifier("빈 이름")

새로 만든 빈 인스턴스를 수정할 수 있는 라이프 사이클 인터페이스로 @BeanPostProcessor가 있다. @PostConstruct를 사용한다.

@BeanPostProcessor의 구현체중 하나로 @AutowiredAnnotationBeanPostProcessor가 있고 이 구현체가 @Autowired, @Value, @Inject 를 지원한다.

내부적으로 BeanFactory@BeanPostProcessor를 이름으로 가진 빈들을 등록한 후 일반 빈 인스턴스를 생성할 때 적용해준다.

@ComponentScan

@ComponentScan은 스프링 구동시 등록할 컴포넌트들을 찾아서 등록해준다.

컴포넌트에 속하는 어노테이션들은 다음과 같다.

  • @Controller
  • @Service
  • @Repository
  • @Configuration

컴포넌트를 스캔할 때는 베이스 클래스이하 모든 패키지를 대상으로 한고 필요에 따라서 필터를 활용하여 컴포넌트를 대상에 포함하거나 제거할 수 있다.

베이스 클래스이외에 존재하는 컴포넌트를 등록하는 방법은 다음과 같다.

빌더 사용

public static void main(String[] args) {
    new SpringApplicationBuilder()
        .sources(Demospring51Application.class)
        .initializers((ApplicationContextInitializer<GenericApplicationContext>)
            applicationContext -> {
                applicationContext.registerBean(MyBean.class);
            }
        )
        .run(args);
}

NEW 사용

public static void main(String[] args) {
    var app = new SpringApplication(Demospring51Application.class)
    app.addInitializers(
        (ApplicationContextInitializer<GenericApplicationContext>) ctx -> {
            ctx.registerBean(MyBean.class);
        }
    )
    app.run(args);
}

Environment - 프로파일

ApplicationContext가 상속받은 인터페이스 중 하나인 EnvironmentCapable의 기능으로 빈을 그룹으로 묶을 수 있는 기능이다.

Config를 활용하는 경우

@Configuration
@Profile("test")
public class config() {
    @Bean 
    public BookRepository bookRepository() {
        return new TestBookRepository();
    }
}

클래스에 직접 정의하는 경우

@Repository
@Profile("test")
public class TestBookRepository implements BookRepository {
}

Environment - 프로퍼티

ApplicationContext가 상속받은 인터페이스 중 하나인 EnvironmentCapable의 기능으로 외부 설정값을 불러올 수 있는 기능이다.

JVM 옵션을 설정은 다음과 같이 할 수 있다.

-Dapp.name=spring5

외부 설정파일을 참조하려면 다음과 같이 할 수 있다.

@PropertySource("classpath:/app.properties")

사용법은 다음과 같다.

@Autowired
ApplicationContext ctx;

Environment environment = ctx.getEnvironment();
environment .getProperty("app.name");

MessageSource

ApplicationContext가 상속받은 인터페이스 중 하나인 MessageSource의 기능으로 다국어 메시지를 지원하는 기능이다. - 국제화 (i18n)

Springboot를 사용한다면 별다른 설정없이 파일을 추가해주면 된다.

  • messages.properties
  • messages_ko_KR.properties
  • ...

사용법은 다음과 같다.

# messages.properties
greeting=Hello {0}
# messages_ko_KR.properties
greeting=안녕, {0}
@Autowired
MessageSource messageSource;

messageSource.getMessage("greeting", new Stinrg[]{"Andy"}, Locale.KOREA);

ApplicationEventPublisher

ApplicationContext가 상속받은 인터페이스 중 하나인 ApplicationEventPublisher의 기능으로 이벤트 프로그래밍에 필요한 인터페이스를 제공한다. - 옵저버 패턴

Spring 4.2 전

ApplicationEvent를 상속받은 이벤트를 생성하고 ApplicationListener<이벤트 객체>를 구현한 클래스를 만들어서 이벤트 처리를 해야했다.

@Component
public class MyEvent extends ApplicationEvent {
}

@Component
public class MyEventHandler implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
    }
}

// 이벤트 생성
@Autowired
ApplicationEventPublisher eventPublisher;

eventPublisher.publishEvent(new MyEvent(this, 100));

Spring 4.2 이후

ApplicationEventApplicationListener<이벤트 객체>없이 이벤트 프로그래밍을 하도록 지원한다.

구현체는 일반 빈으로 생성하고 리스닝하는 함수에 @EventListener를 선언해주면 동작한다.

@EventListener
public void handle(MyEvent event) {
}

동일한 이벤트에 다수의 리스너가 등록되어 있으면 모두 동기적으로 호출된다.

이벤트 리스너간 순서를 정해주고 싶으면 @Order와 함께 사용하자

@EventListener
@Order(Ordered.LOWEST_PRECEDENCE)

비동기로 호출하고 싶을때는 @Async를 사용하자

@EnableAsync
public class ...

@EventListener
@Async
public void handle ...

스프링에서 지원하는 이벤트도 있다

  • ContextRefreshedEvent
  • ContextStartedEvent
  • ContextStoppedEvent
  • ContextClosedEvent
  • RequestHandledEvent
  • ...

ResourceLoader

ApplicationContext가 상속받은 인터페이스 중 하나인 ResourceLoader의 기능으로 리소스를 읽어오는 가능을 제공하는 인터페이스를 제공한다.

사용법은 다음과 같다.

@Autowired
ResourceLoader resourceLoader;

Resource resource = resourceLoader.getResource("classpath:text.txt")
resource.exists();
Files.readString(Path.of(resource.getURI()));

스프링의 IoC 컨테이너가 관리하는 객체로 기본설정이 싱글톤으로 관리된다.

스코프

빈의 스코프는 기본적으로 싱글톤으로 생성되지만 프로토타입으로 생성할 수 있다.

@Scope(value = "prototype")

그러면 호출시마다 빈을 생성하여 사용한다.

주의할 점은 빈의 생명주기가 짧은 빈이 긴 빈을 내부 변수로 사용할 때는 문제가 없으나, 생명주기가 긴 빈이 짧은 빈을 내부 변수로 사용할 때는 의도치 않게 동작할 수 있다.

@Component
public class Single {
    @Autowired
    private Proto proto;
}

...

@Component @Scope(value = "prototype")
public class Proto {
}

...

@Autowired
ApplicationContext ctx;

public static void main(String[] args) {
    ctx.getBean("single").getProto();
    ctx.getBean("single").getProto();
    // 프로토타입 스코프의 빈이지만 동일한 객체 반환
}

싱글톤 객체에서 프로토타입객체를 매번 다른 객체로 반환하게 하고 싶으면 @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)처럼 프록시 감싸서 사용하면 된다.

proxyMode를 사용하면 Spring에서 빈을 생성할 때 cglib 프록시로 생성한다.

다른 방법으로는 Object-Provider를 사용하면 된다.

@Component
public class Single {
    @Autowired
    private ObjectProvider<Proto> proto;

    public Proto getProto() {
        return proto.getIfAvailable();
    }
}

proxyMode를 사용하는게 코드를 Pojo하게 가져갈 수 있기 때문에 추천한다.

싱글톤 객체를 사용할 때는 Thread-Safe하게 코딩하는 습관이 중요하다.

Resource / Validation

Resource 추상화

스프링 내부에서 많이 사용하는 인터페이스로 java.net.URL을 추상화 한 것이다.

추상화를 한 이유는 Classpath를 기준으로 리소스를 읽어오는 기능이 부재했고, ServletContext를 기준으로 상대 경로로 읽어오는 기능의 부재 등 평의성을 높이기 위한 것이다.

구현체는 다음과 같다.

  • UrlResource
  • ClassPathResource
  • FileSystemResource
  • ServletContextResource

Resource의 타입은 location 문자열(접두어)과 ApplicationContext의 타입에 따라 결정되는데

  • ClassPathXmlApplicationContext -> ClassPathResource
  • FileSystemXmlApplicationContext -> FileSystemResource
  • WebApplicationContext -> ServletContextResource
    와 같이 Resource 타입이 결정된다.

location에 접두어를 사용하면 ApplicationContext의 타입에 상관없이 Resource 타입을 설정할 수 있다.

예제

@Autowired
ResourceLoader resourceLoader;

Resource resource = resourceLoader.getResource("classpath:text.txt")
resource.exists();
Files.readString(Path.of

Validation 추상화

어플리케이션에서 사용하는 객체 검증용 인터페이스이다.

org.springframework.validation.Validatorimplements하여 구현할 수 있다.

public class Event {
    String title;
    Integer limit;
    String email;
    ...
}
public class EventValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Event.class.equals(clazz);
    }
    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "title", "notempty", "Empty title is now allowed.");
    }
}

실행

Event event = new Event();
EventValidator eventValidator = new EventValidator();
Errors errors = new BeanPropertyBindingResult(event, "event");

eventValidator.validate(event, errors);

errors.hasErrors();
errors.getAllErrors().forEach(e -> {
    Arrays.stream(e.getCodes()).forEach(System.out::println);
    e.getDefaultMessage();
});

Springboot 2.0.5이상에서는 Validator구현할 필요 없이 LocalValidatorFatoryBean 빈이 자동으로 등록되어 org.springframework.validation.Validator@Autowired로 의존성을 주입하여 사용할 수 있다.

public class Event {
    @NotEmpty
    String title;
    @NotNull @Min(0)
    Integer limit;
    @Email
    String email;
    ...
}

실행

@Autowired
Validator validator;

Event event = new Event();
Errors errors = new BeanPropertyBindingResult(event, "event");

validator.validate(event, errors);

errors.hasErrors();
errors.getAllErrors().forEach(e -> {
    Arrays.stream(e.getCodes()).forEach(System.out::println);
    e.getDefaultMessage();
});

데이터 바인딩

데이터 바인딩 추상화

데이터 바인딩이란 Text형태의 데이터를 객체로 변환해주는것을 말한다.

Spring 3.0 이전

구현하여 사용할 수 있으나 구현해야될 함수가 많기 때문에 `PropertyEditorSupport를 상속받아 사용할 수 있다.

public class EventEditor extends 
PropertyEditorSupport {  
    @Override  
    public String getAsText(){  
        Event event = (Event)getValue();
        return event.getId().toString();
    }
    @Override  
   public void setAsText(String text) throws IllegalArgumentException {
       setValue(new Event(Integer.parseInt(text)));  
  }
}

Pro 가 가지는 ValueThread-Safe하지 않아 싱글톤 스코프의 빈으로 등록하여 하용하면 안된다.

쓰레드 스코프로 빈을 등록하던가 왠만하면 빈으로 등록하지 말고 빈마다 바인더를 등록하여 사용하는것을 추천한다.

빈에 바인더를 등록하는 예제

@Initinder  
public void initWebDainder webDatainder) {
    webDatainder.registerustomEditor(Event.class new EventEditor());  
}

또 하나, PropertyEditorObjectString간의 변환만 가능하다는 특성이 있다.

Spring 3.0 부터 지원 - Converter, Formatter

PropertyEditorThread-Safe하지 않고 ObjectString간의 변환만 가능하다는 제한 때문에 Converter, Formatter를 제공한다.

Converter

S타입을 T타입으로 변환할 수 있는 generic한 변환기이고 Thread-Safe하며 ConverterRegistry에 등록해서 사용한다.

public class EventConverter {  

    public static class StringToEventConverter implements Converter<String, Event> {  
        @Override  
        public Event convert(String source){  
            return new Event(Integer.parseInt(source));  
        }  
    }  

    public static class EventToStringConverter implements Converter<Event, String>{  
        @Override  
        public String convert(Event source){  
            return source.getId().toString();  
        }  
    }  
}
@Configuration  
public class WebConfig implements WebMvcConfigurer {  

    @Override  
    public void addFormatters(FormatterRegistry registry) {  
        registry.addConverter(new EventConverter.StringToEventConverter());  
    }  
}
Formatter

PropertyEditor의 대체재로 ObjectString간의 변환을 담당하고 FormatterRegistry에 등록해서 사용한다.

문자열을 locale에 따라 다국화하는 기능도 지원한다.

public class EventFormatter implements Formatter<Event> {  

    @Override  
    public Event parse(String text, Locale locale) throws ParseException {  
        return new Event(Integer.parseInt(text));  
    }  

    @Override  
    public String print(Event object, Locale locale) {  
        return object.getId().toString();  
    }  
}
@Configuration  
public class WebConfig implements WebMvcConfigurer {  

    @Override  
    public void addFormatters(FormatterRegistry registry) {  
        registry.addFormatter(new EventFormatter());  
    }  
}
Conversion Service

ConverterFormatter의 실제 변환작업이 이뤄지는 곳으로 Thread-Safe 하게 동작한다.

DefaultFormattingConversionService가 구현체로 빈으로 등록되어 있으며 ConversionServiceFormatterRegistry를 상속받아 사용한다.

Springboot의 경우에는 웹 어플리케이션인 경우에는 WebConversionServiceDefaultFormattingConversionService를 상속받아서 제공되고 ConverterFormatter 빈을 찾아 등록해준다.

public class EventConverter {  

    @Component
    public static class StringToEventConverter implements Converter<String, Event> {  
        @Override  
        public Event convert(String source){  
            return new Event(Integer.parseInt(source));  
        }  
    }  

    @Component
    public static class EventToStringConverter implements Converter<Event, String>{  
        @Override  
        public String convert(Event source){  
            return source.getId().toString();  
        }  
    }  
}
@Component
public class EventFormatter implements Formatter<Event> {  

    @Override  
    public Event parse(String text, Locale locale) throws ParseException {  
        return new Event(Integer.parseInt(text));  
    }  

    @Override  
    public String print(Event object, Locale locale) {  
        return object.getId().toString();  
    }  
}

SpEL

SpEL (Spring Expression Language)

스프링 3.0부터 지원하는 기능이고 객체 그래프 조회 및 메소드 호출, 문자열 템플릿 등의 기능을 제공한다.

문법은 다음과 같다.

  • #(표현식)
  • $(프로퍼티)
  • 표현식에는 프로퍼티를 사용할 수 있지만 반대는 안된다.

사용 예제는 다음과 같다.

@Component  
public class AppRunner implements ApplicationRunner {  

    @Value("#{1+1}")  
    int value;  

    @Value("#{'Hello ' + ' World'}")  
    String hello;  

    @Value("#{1 eq 1}")  
    boolean trueOrFalse;  

    @Value("you are great")  
    String great;  

    @Value("${my.val}")
    String myName;  

    @Value("#{${my.val} eq 100}")
    boolean isMyNameSaelobi;  

    @Value("#{sample.data}")
    int sampleData;
}

Expression Parser 사용 예제

@Component  
public class AppRunner implements ApplicationRunner {  

    @Override  
    public void run(ApplicationArguments args) throws Exception {  
        ExpressionParser parser = new SpelExpressionParser();  
        Expression expression = parser.parseExpression("2 + 100");
        Integer value = expression.getValue(Integer.class);
        System.out.println(value);  
  }  
}

스프링 AOP

AOP (Aspect-Oriented Programming)

관점 지향 프로그래밍으로 OOP에서 흩아진 관심사를 모듈화할 수 있는 프로그래밍 기법이다.

주요 개념은 다음과 같다.

  • Aspect와 Target
  • Advice
  • Join point와 Pointcut

자바의 대표적인 구현체로는 AspectJ스프링 AOP가 있는데 용할 수 있는 스프링 AOP의 사용을 추천한다.

적용 시점은 다음과 같이 구분할 수 있다.

  • 컴파일
    • 컴파일 시점에 AOP코드를 클래스 파일에 바이트 코드로 넣는 작업을 한다.
    • 별도의 컴파일 작업이 필요하다.
  • 로드 타임
    • 클래스 로드시 로직을 추가하여 메모리에 올리는 작업을 한다. - 로드타임 위빙(Weaving)
    • 로드타임 위버에 대한 설정을 해야하지만 AspectJ를 활용하여 다양한 문법을 활용할 수 있다.
  • 런타임
    • 빈 생성시 Proxy 빈을 생성한다.
    • 별도의 설정이 필요없고 문법이 쉬운 장점이 있다.
    • 스프링 AOP를 사용한다.

프록시 기반 AOP

스프링 AOP의 특징은 다음과 같다.

  • 프록시 기반의 AOP 구현체이다
  • 스프링 빈에만 AOP를 적용할 수 있다.
  • 모든 AOP 기능을 제공하는 것이 목적이 아니라, 스프링 IoC와 연동하여 엔터프라이즈 어플리케이션에서 가장 흔한 문제에 대한 해결책을 제공하는 것이 목적이다.

프록시 패턴은 기존 코드를 건드리지 않고 접근 제어 또는 부가기능을 제어할 수 있다.

하지만 매번 프록시 클래스를 작성해야하는 문제점이 있어서 스프링 AOP가 개발됐다.

스프링 AOP

IoC 컨테이너가 제공하는 기반 시설과 Dynamic 프록시를 사용하여 복잡한 문제를 해결했다.

동적 프록시란?
런타임시 프록시 객체를 생성하는 방법이다. 자바가 제공하는 방법은 인터페이스 기반 프록시 생성으로 CGlib는 클래스 기반 프록시도 지원한다.

스프링 IoC는 기존 빈을 대체하는 동적 프록시 빈을 만들어 등록 시켜준다.

프록시 패턴의 예제

public interface EventService {

    void createEvent();

    void publishEvent();

    void deleteEvent();
}

@Service
public class SimpleEventService implements EventService {

    @Override
    public void createEvent() {
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");
    }

    @Override
    public void publishEvent() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("Published an event");
    }

    @Override
    public void deleteEvent() {
        System.out.println("Delete an event");
    }
}

@Primary
@Service
public class ProxySimpleEventService implements EventService {

    @Autowired
    SimpleEventService simpleEventService;

    @Override
    public void createEvent() {
        long begin = System.currentTimeMillis();
        simpleEventService.createEvent();
        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void publishEvent() {
        long begin = System.currentTimeMillis();
        simpleEventService.publishEvent();
        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void deleteEvent() {
        simpleEventService.deleteEvent();
    }
}

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    EventService eventService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        eventService.createEvent();
        eventService.publishEvent();
        eventService.deleteEvent();
    }
}

스프링 AOP

스프링에서 제공하는 어노테이션 기반 AOP이다.

사용하기 위해서는 의존성을 추가해야 한다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring.-boot-starter-aop</artifactId>
</dependency>

@Aspect를 선언하여 애스펙트로 정의한다.

@Component
@Aspect
public class PerfAspect {

    @Around("@annotation(PerLogging)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable{
        long begin = System.currentTimeMillis();
        Object retVal = pjp.proceed(); // 메서드 호출 자체를 감쌈
        System.out.println(System.currentTimeMillis() - begin);
        return retVal;
    }
}

Pointcut을 표현식으로 해도 되지만 어노테이션 기반으로도 사용할 수 있다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerLogging {
}

생성한 어노테이션을 선언하면 해당 함수에 대해서 Aspect가 동작한다.

@Service
public class SimpleEventService implements EventService {

    @PerLogging
    @Override
    public void createEvent() {
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");
    }

    @PerLogging
    @Override
    public void publishEvent() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("Published an event");
    }

    @Override
    public void deleteEvent() {
        System.out.println("Delete an event");
    }
}

Null-Safety

Null-Safety

스프링 프레임워크 5에 추가된 Null 관련 어노테이션이다.

종류는 다음과 같다.

  • @NonNull
  • @Nullable
  • @NonNullApi (패키지 레벨에 설정)
  • @NunNullFields (패키지 레벨에 설정)

목적은 툴의 지원을 받아 컴파일 시점에 최대한 NullPointException을 방지하는 것이다.

후기

이전에 김영하님의 강좌를 듣고 이어서 들은 강좌여서 그런지 상대적으로 백기선님이 프리한 분위기로 강의하신다는 느낌을 받았고 그래서 좀더 정감가는(?) 느낌을 받아 좋았습니다.

저 같은 경우는 스프링을 주먹구구식으로 사용하다가 스프링부트를 주먹구구식으로 사용하고 있었는데요.

그 동안 단편적으로 추측했던 내용들의 스토리로 엮이면서 정리된거 같아 좋았습니다.

특히 IoC 컨테이너나 데이터 바인더쪽을 들으면서 빈들이 어떻식으로 관리되고 메시지가 객체로 변환되는 과정을 들으면서 그 동안 의문으로만 가지고 있던 부분이 해소돼서 좋았습니다.

또한 스프링을 공부하지먼 스프링부트 기반으로 설명해주셔서 스프링부트에 대한 감도 잡을 수 있어서 좋았네요.

저처럼 스프링에 대해 정리하고 싶으신 분들께 강추하는 강좌입니다.잡을 수 있어서 좋았네요.

저처럼 스프링에 대해 정리하고 싶으신 분들께 강추하는 강좌입니다.

Comments