[Naver Cloud Camp 7] 교육 정리

네이버 클라우드 캠프 69일차 230801

우기37 2023. 8. 1. 20:47

#1 교육정리

1) IoC 컨테이너 적용하기

 

 

 

#2 IoC 컨테이너 적용하기

이미지 출처 : http://ojc.asia/bbs/board.php?bo_table=LecSpring&wr_id=861

Spring IoC)

마틴 파울러는 2004년의글에서 제어의 어떤 측면이 역행되는 것인지에 대한 의문을 제기하고 의존하는 객체를 역행적으로 취득하는 것이라는 결론을 내렸다.  그는 그와 같은 정의에 기초하여 제어 역행이라는 용어에 좀더 참신한 ‘의존성 주입(DI,dependency injection)’이라는 이름을 지어줬다.

IoC(Inversion of Control : 제어의 역전)는 디자인 패턴, 팩토리 패턴 및 DI (Dependency Injection)와 같은 메커니즘을 통해 달성 할 수 있다.

 

즉, 객체의 생성, 생명주기의 관리까지 모든 객체에 대한 제어권이 바뀌었다는 것을 의미한다.

 

컴포넌트 의존관계 설정(Component dependency resolution), 설정(Configuration) 및 생명주시(LifeCycle)를 해결하기 위한 디자인 패턴 입니다.

 

기존에 자바 기반으로 어플리케이션을 개발할 때 자바 객체를 생성하고 서로간의 의존관계를 연결시키는 작업에 대한 제어권은 보통 개발되는 어플리케이션에 있었다. 그러나 Servlet, EJB 등을 사용하는 경우 Servlet Container, EJB Container에게 제어권이 넘어가서 객체의 생명주기를 Container들이 전담하게 된다. 다시 말해서 제어의 역전이란 인스턴스 생성부터 소멸까지의 인스턴스 생명주기 관리를 개발자가 아닌 컨테이너가 대신 해준다는 의미이다.

 

POJO의 생성, 초기화, 서비스, 소멸에 대한 권한을 가진다. 그래서 개발자들이 직접 POJO를 생성할 수 있지만, 컨테이너에게 맡겨 다른 클래스에 영향을 끼치지 않으면서 변경이 가능해 유지보수에 효율적인 관리가 가능하다.

 

 

이미지 출처 : https://jjunii486.tistory.com/84

주로 특정 자바 모델이나 기능, 프레임워크를 따르지 않는 Java Object를 지칭한다.

예를 들자면, getter / setter를 빗대오 보면 됩니다.

 

 

컨테이너의 종류)

 

스프링 컨테이너의 종류는 두가지로 나누어져 있습니다.

 

BeanFactory)

객체를 생성하고, 객체 사이의 런타임 의존관계를 맺어주는 역할을 하는 스프링 컨테이너의 최상위 인터페이스이다.

 

Bean을 등록, 생성, 조회, 반환 관리를 한다.
팩토리 디자인 패턴을 구현한 것으로 BeanFactory는 빈을 생성하고 분배하는 책임을 지는 클래스이다.
Bean을 조회할 수 있는 getBean() 메소드가 정의되어 있다. 보통 getBean()이 호출되면, 팩토리는 의존성 주입(DI)을 이용해 빈을 객체로 만들어 빈의 특성을 설정하기 시작한다. 이것을 빈의 일생이 시작된다고 이야기하기도 한다.
보통은 BeanFactory를 바로 사용하지 않고, 이를 확장한 ApplicationContext를 사용한다.

 

ApplicationContext)

Bean Factory를 포함한 여러 인터페이스들을 상속받은 인터페이스로, 스프링 컨테이너라고 하면 일반적으로 ApplicationContext를 가리킨다. BeanFactory보다 ApplicationContext를 더 많이 사용하기 때문이다.

 

Bean을 등록, 생성, 조회, 반환 관리하는 기능은 BeanFactory와 같다. 결국 빈 팩토리를 상속받았기 때문에 빈 팩토리를 확장한 컨테이너라고 생각하면 좋다.
기본적인 기능은 BeanFactory와 동일하고, 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다.
국제화가 지원되는 텍스트 메시지를 관리해준다.
이미지같은 파일 자원을 로드 할 수 있는 포괄적인 방법을 제공해준다.
리너스로 등록된 빈에게 이벤트 발생을 알려준다.
ApplicationContext 종류에는 2가지가 있다. root-applicationContext와 servlet-applicationContext이다.

 

 

이미지 출처 : https://www.educba.com/ioc-containers/

 

BeanFactory와 ApplicationContext 차이점)

 BeanFactory :
처음으로 getBean()이 호출된 시점에서야 해당 빈을 생성한다.

ApplicationContext :
컨텍스트 초기화 시점에 모든 싱글톤 빈을 미리 로드한 후 애플리케이션 기동 후에는 빈을 지연 없이 얻을 수 있다. 즉, 미리 빈을 생성해 높아서 빈이 필요할 떄 즉시 사용할 수 있도록 보장하는 것이다.

때문에 대부분의 애플리케이션에서는 빈팩토리 보다는 ApplicationContext를 더 많이 사용한다.

 

 

IoC와 DI의 관계)

DI :

자바로 프로그래밍을 하면서 객체를 생성할 때 직접 클래스에 new 연산자를 이용하여 생성했습니다. 하지만 DI는 개발자가 직접 코딩을 하여 객체를 생성하는 것이 아니라, 컨테이너가 이를 생성시켜 주는 것입니다. 그렇게 된다면 코드에서 직접적인 연관 관계가 발생하지 않아 각 클래스들의 변경이 자유로워 집니다. 이를 느슨한 결합이라고 합니다.

 

각 클래스들간 결합도가 높게되면 나중에 프로젝트가 복잡해질시 유지보수가 힘들게 됩니다. 따라서 각 클래스들간 연관 관계를 클래스 자체 내에서 맺어주는 것이 아니라 스프링 자체에서 설정을 통해 연관 관계를 맺어줌으로써 결합도를 낮춰줍니다.

 

DI를 사용했을 때의 장점 : 

- 클래스들 간 의존 관계를 최소화 할 수 있다.
- 프로젝트 유지보수가 용이하다.
- 기존에는 개발자가 직접 객체의 생성과 소멸을 제어했는데 DI로 인해 객체의 생성과 소멸 등 클래스간 의존관계를 스프링 컨테이너가 제어해준다.


DI는 객체의 생성, 소멸, 의존 관계를 개발자가 직접 설정하는 것이 아니라 XML이나 애너테이션을 통해 스프링 프레임워크가 제어합니다. 기존에는 개발자가 직접 객체를 생성해줬던 반면에 스프링 프레임워크에서는 객체의 제어를 스프링이 직접 담당해주는 IoC 특징을 가집니다.

 

스프링에서는 의존하는 객체를 컨테이너 실행 시 객체를 주입하기 때문에 DI라고 부릅니다. 여기서 각 클래스 객체를 Bean(이하 빈)이라고 부르는데 이는 의존관계를 설정하는 xml 파일에서 각각의 객체를 <bean> 태그로 표시하기 때문입니다.

 

이미지 출처 : https://jjunii486.tistory.com/84

 

코드로 적용하기)

1. Application을 실행하는데 필요한 객체를 설정하고 Mybatis 객체를 요청하는 config class를 생성합니다.

package bitcamp.myapp_project.config;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import bitcamp.util.Bean;
import bitcamp.util.ComponentScan;
import bitcamp.util.SqlSessionFactoryProxy;

// Application을 실행하는데 필요한 객체를 설정하는 일을 한다.
//
@ComponentScan(basePackages = {"bitcamp.myapp_project.dao", "bitcamp.myapp_project.handler"})
public class PatientAppConfig {

  // Mybatis 객체 준비
  @Bean
  public SqlSessionFactory sqlSessionFactory() throws Exception {
    return new SqlSessionFactoryProxy(new SqlSessionFactoryBuilder()
        .build(Resources.getResourceAsStream("bitcamp/myapp/config/mybatis-config.xml")));
  }
}

 

2. HttpServletRequest 를 요청하는 class 생성

package bitcamp.util;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.multipart.HttpData;
import reactor.core.publisher.Flux;
import reactor.netty.ByteBufFlux;
import reactor.netty.Connection;
import reactor.netty.http.server.HttpServerFormDecoderProvider.Builder;
import reactor.netty.http.server.HttpServerRequest;

public class HttpServletRequest {
  HttpServerRequest original;

  public HttpServletRequest(HttpServerRequest original) {
    this.original = original;
  }

  public ByteBufFlux receive() {
    return original.receive();
  }

  public Map<CharSequence, List<Cookie>> allCookies() {
    return original.allCookies();
  }

  public Map<CharSequence, Set<Cookie>> cookies() {
    return original.cookies();
  }

  public Flux<?> receiveObject() {
    return original.receiveObject();
  }

  public String fullPath() {
    return original.fullPath();
  }

  public SocketAddress connectionHostAddress() {
    return original.connectionHostAddress();
  }

  public String requestId() {
    return original.requestId();
  }

  public HttpServerRequest withConnection(Consumer<? super Connection> withConnection) {
    return original.withConnection(withConnection);
  }

  public String param(CharSequence key) {
    return original.param(key);
  }

  public SocketAddress connectionRemoteAddress() {
    return original.connectionRemoteAddress();
  }

  public Map<String, String> params() {
    return original.params();
  }

  public boolean isKeepAlive() {
    return original.isKeepAlive();
  }

  public boolean isWebsocket() {
    return original.isWebsocket();
  }

  public HttpServerRequest paramsResolver(
      Function<? super String, Map<String, String>> paramsResolver) {
    return original.paramsResolver(paramsResolver);
  }

  public String scheme() {
    return original.scheme();
  }

  public HttpMethod method() {
    return original.method();
  }

  public String path() {
    return original.path();
  }

  public String connectionScheme() {
    return original.connectionScheme();
  }

  public Flux<HttpContent> receiveContent() {
    return original.receiveContent();
  }

  public String hostName() {
    return original.hostName();
  }

  public boolean isFormUrlencoded() {
    return original.isFormUrlencoded();
  }

  public int hostPort() {
    return original.hostPort();
  }

  public String uri() {
    return original.uri();
  }

  public boolean isMultipart() {
    return original.isMultipart();
  }

  public HttpVersion version() {
    return original.version();
  }

  public Flux<HttpData> receiveForm() {
    return original.receiveForm();
  }

  public Flux<HttpData> receiveForm(Consumer<Builder> formDecoderBuilder) {
    return original.receiveForm(formDecoderBuilder);
  }

  public InetSocketAddress hostAddress() {
    return original.hostAddress();
  }

  public InetSocketAddress remoteAddress() {
    return original.remoteAddress();
  }

  public HttpHeaders requestHeaders() {
    return original.requestHeaders();
  }

  public String protocol() {
    return original.protocol();
  }

  public ZonedDateTime timestamp() {
    return original.timestamp();
  }

}

 

 

3. HttpServletResponse 를 응답하는 class 생성

package bitcamp.util;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.SocketAddress;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.cookie.Cookie;
import reactor.core.publisher.Mono;
import reactor.netty.Connection;
import reactor.netty.NettyOutbound;
import reactor.netty.http.server.HttpServerResponse;
import reactor.netty.http.server.WebsocketServerSpec;
import reactor.netty.http.websocket.WebsocketInbound;
import reactor.netty.http.websocket.WebsocketOutbound;

public class HttpServletResponse {

  HttpServerResponse original;
  // 서블릿이 클라이언트에게 응답할 때 사용할 출력스트림 도구
  StringWriter buf = new StringWriter();
  PrintWriter out;

  // 응답 콘텐트의 타입
  String contentType = "text/plain;charset=ISO-8859-1";

  public HttpServletResponse(HttpServerResponse original) {
    this.original = original;
  }

  public PrintWriter getWriter() {
    if (out == null) {
      out = new PrintWriter(buf);
    }
    return this.out;
  }

  public String getContent() {
    return buf.toString();
  }

  public void setContentType(String contentType) {
    if (out != null) {
      // 출력 스트림을 사용한 상태라면 콘텐트 설정을 무시한다.
      return;
    }
    this.contentType = contentType;
  }

  public String getContentType() {
    return this.contentType;
  }

  public SocketAddress hostAddress() {
    return original.hostAddress();
  }

  public Map<CharSequence, List<Cookie>> allCookies() {
    return original.allCookies();
  }

  public Map<CharSequence, Set<Cookie>> cookies() {
    return original.cookies();
  }

  public String fullPath() {
    return original.fullPath();
  }

  public SocketAddress connectionHostAddress() {
    return original.connectionHostAddress();
  }

  public String requestId() {
    return original.requestId();
  }

  public HttpServerResponse addCookie(Cookie cookie) {
    return original.addCookie(cookie);
  }

  public SocketAddress remoteAddress() {
    return original.remoteAddress();
  }

  public HttpServerResponse addHeader(CharSequence name, CharSequence value) {
    return original.addHeader(name, value);
  }

  public SocketAddress connectionRemoteAddress() {
    return original.connectionRemoteAddress();
  }

  public boolean isKeepAlive() {
    return original.isKeepAlive();
  }

  public HttpServerResponse chunkedTransfer(boolean chunked) {
    return original.chunkedTransfer(chunked);
  }

  public boolean isWebsocket() {
    return original.isWebsocket();
  }

  public String scheme() {
    return original.scheme();
  }

  public HttpServerResponse withConnection(Consumer<? super Connection> withConnection) {
    return original.withConnection(withConnection);
  }

  public HttpMethod method() {
    return original.method();
  }

  public ByteBufAllocator alloc() {
    return original.alloc();
  }

  public HttpServerResponse compression(boolean compress) {
    return original.compression(compress);
  }

  public String path() {
    return original.path();
  }

  public Mono<Void> neverComplete() {
    return original.neverComplete();
  }

  public String connectionScheme() {
    return original.connectionScheme();
  }

  public String hostName() {
    return original.hostName();
  }

  public boolean hasSentHeaders() {
    return original.hasSentHeaders();
  }

  public NettyOutbound send(Publisher<? extends ByteBuf> dataStream) {
    return original.send(dataStream);
  }

  public HttpServerResponse header(CharSequence name, CharSequence value) {
    return original.header(name, value);
  }

  public int hostPort() {
    return original.hostPort();
  }

  public String uri() {
    return original.uri();
  }

  public HttpServerResponse headers(HttpHeaders headers) {
    return original.headers(headers);
  }

  public HttpVersion version() {
    return original.version();
  }

  public HttpServerResponse keepAlive(boolean keepAlive) {
    return original.keepAlive(keepAlive);
  }

  public NettyOutbound send(Publisher<? extends ByteBuf> dataStream, Predicate<ByteBuf> predicate) {
    return original.send(dataStream, predicate);
  }

  public HttpHeaders responseHeaders() {
    return original.responseHeaders();
  }

  public Mono<Void> send() {
    return original.send();
  }

  public NettyOutbound sendHeaders() {
    return original.sendHeaders();
  }

  public Mono<Void> sendNotFound() {
    return original.sendNotFound();
  }

  public NettyOutbound sendByteArray(Publisher<? extends byte[]> dataStream) {
    return original.sendByteArray(dataStream);
  }

  public Mono<Void> sendRedirect(String location) {
    return original.sendRedirect(location);
  }

  public Mono<Void> sendWebsocket(
      BiFunction<? super WebsocketInbound, ? super WebsocketOutbound, ? extends Publisher<Void>> websocketHandler) {
    return original.sendWebsocket(websocketHandler);
  }

  public NettyOutbound sendFile(Path file) {
    return original.sendFile(file);
  }

  public Mono<Void> sendWebsocket(
      BiFunction<? super WebsocketInbound, ? super WebsocketOutbound, ? extends Publisher<Void>> websocketHandler,
      WebsocketServerSpec websocketServerSpec) {
    return original.sendWebsocket(websocketHandler, websocketServerSpec);
  }

  public HttpServerResponse sse() {
    return original.sse();
  }

  public NettyOutbound sendFile(Path file, long position, long count) {
    return original.sendFile(file, position, count);
  }

  public HttpResponseStatus status() {
    return original.status();
  }

  public HttpServerResponse status(HttpResponseStatus status) {
    return original.status(status);
  }

  public HttpServerResponse status(int status) {
    return original.status(status);
  }

  public HttpServerResponse trailerHeaders(Consumer<? super HttpHeaders> trailerHeaders) {
    return original.trailerHeaders(trailerHeaders);
  }

  public NettyOutbound sendFileChunked(Path file, long position, long count) {
    return original.sendFileChunked(file, position, count);
  }

  public NettyOutbound sendGroups(Publisher<? extends Publisher<? extends ByteBuf>> dataStreams) {
    return original.sendGroups(dataStreams);
  }

  public NettyOutbound sendObject(Publisher<?> dataStream) {
    return original.sendObject(dataStream);
  }

  public NettyOutbound sendObject(Publisher<?> dataStream, Predicate<Object> predicate) {
    return original.sendObject(dataStream, predicate);
  }

  public NettyOutbound sendObject(Object message) {
    return original.sendObject(message);
  }

  public NettyOutbound sendString(Publisher<? extends String> dataStream) {
    return original.sendString(dataStream);
  }

  public NettyOutbound sendString(Publisher<? extends String> dataStream, Charset charset) {
    return original.sendString(dataStream, charset);
  }

  public <S> NettyOutbound sendUsing(Callable<? extends S> sourceInput,
      BiFunction<? super Connection, ? super S, ?> mappedInput, Consumer<? super S> sourceCleanup) {
    return original.sendUsing(sourceInput, mappedInput, sourceCleanup);
  }

  public void subscribe(Subscriber<? super Void> s) {
    original.subscribe(s);
  }

  public Mono<Void> then() {
    return original.then();
  }

  public NettyOutbound then(Publisher<Void> other) {
    return original.then(other);
  }

  public NettyOutbound then(Publisher<Void> other, Runnable onCleanup) {
    return original.then(other, onCleanup);
  }

}

 

4. 메소드에 붙일 Bean 애노테이션 생성

package bitcamp.util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 메서드에 붙일 애노테이션

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Bean {
  String value() default "";
}

 

5. 각 Servlet에 붙일 Component 애노테이션 생성

package bitcamp.util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 클래스에 붙일 애노테이션

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
  String value() default "";
}

 

6. ComponentScan 애노테이션 생성

package bitcamp.util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ComponentScan {
  String[] basePackages();
}

 

 

7. DispatcherServlet 생성

package bitcamp.util;

import java.io.PrintWriter;

public class DispatcherServlet implements Servlet {

  ApplicationContext iocContainer;

  public DispatcherServlet(ApplicationContext iocContainer) throws Exception {
    this.iocContainer = iocContainer;
  }

  @Override
  public void service(HttpServletRequest request, HttpServletResponse response) throws Exception {


    Servlet servlet = (Servlet) iocContainer.getBean("/" + request.path());
    if (servlet == null) {
      response.setContentType("text/plain;charset=UTF-8");
      PrintWriter out = response.getWriter();
      out.println("해당 요청을 처리할 수가 없습니다!!");
      return;
    }
    servlet.service(request, response);
  }
}

 

 

8. ApplicationContext 클래스 생성

package bitcamp.util;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import bitcamp.myapp.config.AppConfig;

// IoC 컨테이너 = Bean 컨테이너
// - 자바 설정 클래스(예: AppConfig)에서 @Bean 애노테이션이 붙은 메서드를 찾아 호출하고,
// 그 리턴 값을 컨테이너에 보관한다.
// - 자바 설정 클래스(예: AppConfig)에서 @ComponentScan 애노테이션을 찾아서 패키지 정보를 알아낸다.
// 패키지에 소속된 모든 클래스에 대해 인스턴스를 생성하여 컨테이너에 보관한다.
//
public class ApplicationContext {
  // 객체 보관소
  Map<String, Object> beanContainer = new HashMap<>();

  public ApplicationContext(Class<?> configClass) throws Exception {

    processBeanAnnotation(configClass);

    ComponentScan componentScan = configClass.getAnnotation(ComponentScan.class);
    if (componentScan != null) {
      processComponentScanAnnotation(componentScan);
    }
  }

  private void processBeanAnnotation(Class<?> configClass) throws Exception {
    System.out.println("@Bean --------------------------------");

    // 클래스의 기본 생성자를 알아낸다.
    Constructor<?> constructor = configClass.getConstructor();

    // 기본 생성자를 이용하여 객체를 생성한다.
    Object obj = constructor.newInstance();

    // 해당 클래스에 정의된 메서드 목록만 가져온다.
    Method[] methods = configClass.getDeclaredMethods();
    for (Method m : methods) {

      // 메서드의 리턴 타입이 없다면 무시한다.
      if (m.getReturnType() == void.class) {
        continue;
      }

      // @Bean 애노테이션이 붙지 않은 메서드라면 무시한다.
      Bean beanAnnotation = m.getAnnotation(Bean.class);
      if (beanAnnotation == null) {
        continue;
      }

      // 메서드 중에서 리턴 값이 있는 메서드를 호출한다.
      // 즉 오직 값을 리턴하는 메서드만 호출한다.
      Object returnValue = m.invoke(obj);

      // 메서드가 리턴한 값을 컨테이너에 저장한다.
      if (beanAnnotation.value().length() > 0) {
        // 애노테이션에 객체 이름이 지정되어 있다면 그 이름으로 객체를 저장한다.
        beanContainer.put(beanAnnotation.value(), returnValue);
      } else {
        // 애노테이션에 설정된 이름이 없다면 메서드 이름을 사용하여 객체를 저장한다.
        beanContainer.put(m.getName(), returnValue);
      }
      System.out.printf("%s() 객체 생성\n", m.getName());
    }
  }

  private void processComponentScanAnnotation(ComponentScan componentScan) throws Exception {
    for (String basePackage : componentScan.basePackages()) {
      System.out.println(basePackage + "---------------------------------");
      createBeans(basePackage);
    }
  }

  private void createBeans(String basePackage) throws Exception {
    // 패키지 이름에 해당하는 디렉토리 정보를 알아낸다.
    // 1) 패키지 이름을 파일 경로로 변환한다(. --> /)
    String packagePath = basePackage.replaceAll("[.]", "/");

    // 2) 클래스 경로(CLASSPATH)를 관리하는 객체를 준비한다.
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

    // 3) 클래스로더에게 패키지 경로에 해당하는 디렉토리 정보를 읽을 수 있는 도구를 달라고 요구한다.
    InputStream dirInputStream = systemClassLoader.getResourceAsStream(packagePath);
    if (dirInputStream == null) {
      return;
    }

    // 4) 패키지 디렉토리 안에 들어있는 파일 이름이나 하위 디렉토리 이름을 읽을 도구를 준비한다.
    BufferedReader dirReader = new BufferedReader(new InputStreamReader(dirInputStream));

    // 5) 디렉토리 리더를 통해 해당 디렉토리에 들어 있는 하위 디렉토리 또는 파일 이름을 알아낸다.
    // .class 파일을 로딩하여 Class 객체를 준비한다.
    //
    // ==> 전통적인 컬렉션 데이터 가공 방식
    // - 한 줄씩 읽으면 된다.
    // Set<Class<?>> classes = new HashSet<>();
    // String filename = null;
    // while ((filename = dirReader.readLine()) != null) {
    //
    // // 파일 확장자가 .class 로 끝나는 파일만 처리한다.
    // if (filename.endsWith(".class")) {
    //
    // // 패키지 이름과 클래스 이름(.class 확장자를 뺀 이름)을 합쳐서
    // // Fully-Qualified class name을 만든 다음에
    // // Class.forName() 을 사용하여 클래스를 메모리(Method Area)에 로딩한다.
    // Class<?> clazz = Class.forName(basePackage + "." + filename.replace(".class", ""));
    //
    // // 로딩한 클래스 정보를 Set 컬렉션에 담는다.
    // classes.add(clazz);
    // }
    // }

    // ==> 스트림 프로그래밍 기법을 이용하여 컬렉션 데이터를 가공하는 방식
    // Set<Class<?>> classes = dirReader
    // .lines() // 오리지널 문자 단위 스트림을 줄 단위로 문자열로 나열한 스트림으로 변환한다.
    // .filter(new Predicate<String>() { // 스트림에서 처리할 대상을 선택하는 필터를 꼽는다.
    // @Override
    // public boolean test(String filename) {
    // // 이 필터는 파일의 확장자가 .class 로 끝나는 경우에만 처리한다.
    // return filename.endsWith(".class");
    // }})
    // .map(new Function<String, Class<?>>() { // 파일 이름을 받아서 Class 정보를 리턴하는 플러그인 장착한다.
    // @Override
    // public Class<?> apply(String filename) {
    // try {
    // return Class.forName(basePackage + "." + filename.replace(".class", ""));
    // } catch (Exception e) {
    // // 클래스 로딩하다가 예외가 발생하면 무시한다.
    // }
    // return null;
    // }})
    // .collect(Collectors.toSet()); // 스트림을 수행하여 최종 결과물은 Set 컬렉션에 담아서 리턴하라고 명령한다.

    // ==> 스트림 프로그래밍 방식( + 람다)
    Set<Class<?>> classes =
        dirReader.lines().filter(filename -> filename.endsWith(".class")).map(filename -> {
          try {
            return Class.forName(basePackage + "." + filename.replace(".class", ""));
          } catch (Exception e) {
            return null;
          }
        }).collect(Collectors.toSet());

    // 6) 로딩된 클래스 정보를 활용하여 객체를 생성한다.
    for (Class<?> clazz : classes) {
      System.out.println(clazz.getName());
      if (clazz.isEnum() || clazz.isInterface() || clazz.isLocalClass() || clazz.isMemberClass()) {
        // 패키지 멤버 클래스가 아닌 경우 객체 생성 대상에서 제외한다.
        continue;
      }

      Component compAnno = clazz.getAnnotation(Component.class);
      if (compAnno == null) {
        // @Component 애노테이션이 붙지 않았다면 객체 생성 대상에서 제외한다.
        continue;
      }

      // - 클래스 정보를 가지고 클래스의 생성자를 알아낸다.
      Constructor<?> constructor = clazz.getConstructors()[0];

      // - 생성자의 파라미터 정보를 알아낸다.
      Parameter[] params = constructor.getParameters();

      // - 생성자를 파라미터를 가지고 호출할 때 넘겨 줄 아규먼트를 준비한다.
      Object[] args = prepareArguments(params);

      // - 준비한 아규먼트를 가지고 생성자를 통해 객체를 생성한다.
      Object obj = constructor.newInstance(args);

      // - 생성된 객체를 컨테이너에 저장한다.
      if (compAnno.value().length() > 0) {
        // @Component 애노테이션에 객체 이름이 지정되어 있다면 그 이름으로 객체를 저장한다.
        beanContainer.put(compAnno.value(), obj);
      } else {
        // 그렇지 않다면, 클래스 이름으로 객체를 저장한다.
        beanContainer.put(clazz.getSimpleName(), obj);
      }

      System.out.printf("%s 객체 생성!\n", clazz.getName());
    }
  }

  private Object[] prepareArguments(Parameter[] params) {
    // 아규먼트를 담을 컬렉션을 준비한다.
    ArrayList<Object> args = new ArrayList<>();

    for (Parameter param : params) {
      // - 파라미터 타입에 해당하는 객체를 컨테이너에서 찾는다.
      args.add(getBean(param.getType()));
    }

    return args.toArray();
  }

  @SuppressWarnings("unchecked")
  public <T> T getBean(Class<T> type) {
    Collection<?> list = beanContainer.values();
    for (Object obj : list) {
      if (type.isInstance(obj)) {
        return (T) obj;
      }
    }
    return null;
  }

  public Object getBean(String name) {
    return beanContainer.get(name);
  }

  public String[] getBeanNames() {
    return beanContainer.keySet().toArray(new String[0]);
  }

  public static void main(String[] args) throws Exception {
    ApplicationContext applicationContext = new ApplicationContext(AppConfig.class);

    System.out.println("생성된 객체 목록:");
    for (String name : applicationContext.getBeanNames()) {
      System.out.println(name);
    }
  }
}

 

9. ServerApp

package bitcamp.myapp_project;

import bitcamp.myapp_project.config.PatientAppConfig;
import bitcamp.util.ApplicationContext;
import bitcamp.util.DispatcherServlet;
import bitcamp.util.HttpServletRequest;
import bitcamp.util.HttpServletResponse;
import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.NettyOutbound;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.HttpServerRequest;
import reactor.netty.http.server.HttpServerResponse;

public class PatientServerApp {

  ApplicationContext iocContainer;
  DispatcherServlet dispatcherServlet;

  int port;

  public PatientServerApp(int port) throws Exception {

    this.port = port;
    iocContainer = new ApplicationContext(PatientAppConfig.class);
    dispatcherServlet = new DispatcherServlet(iocContainer);
  }

  public void close() throws Exception {

  }

  public static void main(String[] args) throws Exception {
    PatientServerApp app = new PatientServerApp(8888);
    app.execute();
    app.close();
  }

  public void execute() throws Exception {
    DisposableServer server = HttpServer.create().port(8888)
        .handle((request, response) -> processRequest(request, response)).bindNow();
    System.out.println("서버 실행 되었습니다!");

    server.onDispose().block();
    System.out.println("서버 종료되었습니다!");
  }

  private NettyOutbound processRequest(HttpServerRequest request, HttpServerResponse response) {
    try {
      HttpServletRequest request2 = new HttpServletRequest(request);
      HttpServletResponse response2 = new HttpServletResponse(response);
      dispatcherServlet.service(request2, response2);

      // HTTP 응답 프로토콜의 헤더 설정
      response.addHeader("Content-Type", response2.getContentType());

      // 서블릿이 출력한 문자열을 버퍼에서 꺼내 HTTP 프로토콜에 맞춰 응답한다.
      return response.sendString(Mono.just(response2.getContent()));


    } catch (Exception e) {
      e.printStackTrace();
      return response.sendString(Mono.just("Error!"));
    } finally {

    }
  }
}

 

10. ListServlet 코드

package bitcamp.myapp_project.handler;

import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.List;
import bitcamp.myapp_project.dao.PatientDao;
import bitcamp.myapp_project.vo.Patient;
import bitcamp.util.Component;
import bitcamp.util.HttpServletRequest;
import bitcamp.util.HttpServletResponse;
import bitcamp.util.Servlet;

@Component("/patient/list")
public class AnimalHospitalListServlet implements Servlet {

  PatientDao patientDao;
  SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd");

  public AnimalHospitalListServlet(PatientDao patientDao) {
    this.patientDao = patientDao;
  }

  @Override
  public void service(HttpServletRequest request, HttpServletResponse response) throws Exception {
    response.setContentType("text/html;charset=UTF-8");
    PrintWriter out = response.getWriter();
    out.println("<!DOCTYPE html>");
    out.println("<html>");
    out.println("<head>");
    out.println("<meta charset='UTF-8'>");
    out.println("<title>환자목록</title>");
    out.println("</head>");
    out.println("<body>");
    out.println("<h1>동물병원 환자 목록</h1>");
    out.println("<table border='1'>");
    out.println("<thead>");
    out.println(
        "  <tr><th>환자번호</th> <th>보호자번호</th> <th>이름</th> <th>품종</th> <th>나이</th> <th>주소</th> <th>성별</th> <th>등록일</th> </tr>");
    out.println("</thead>");

    int category = 1;
    List<Patient> list = patientDao.findAll(category);

    out.println("<tbody>");
    for (Patient p : list) {
      out.printf(
          "<tr> <td>%d</td>,<td>%d</td>,<td>%s</td>,<td>%s</td>,<td>%d</td>,<td>%s</td>,<td>%s</td>,<td>%d</td> </tr>\n",
          p.getPatientNo(), p.getParentNo(), p.getName(), p.getBreeds(), p.getAge(), p.getAddress(),
          p.getGender(), p.getCreatedDate());
    }
    out.println("</tbody>");
    out.println("</body>");
    out.println("</html>");
  }
}

 

 

 

11. 현재까지 개인 프로젝트 결과를 웹으로 확인하기

http://localhost:8888/patient/list

 

Ioc 컨테이너 적용 후 웹으로 출력하기까지 많은 오류와 부딪히며 구현하였으며, 아직 완성작은 아니지만, IoC 컨테이너의 구동 원리에 대해서 조금 이해하였습니다.