728x90

컴포넌트 스캔과 의존관계 자동 주입 하기를 배워볼 것이다. 지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean 이나 XML 의 <bean> 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다. 예제에서는 몇개가 안되었지만 이렇게 등록해야 할 스프링 빈이 수십, 수백개가 되면서 일일이 등록하기도 귀찮고 설정 정보도 커지고, 누락하는 문제도 발생한다. 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다. 또 의존관계도 자동으로 주입하는 @Autowired 라는 기능도 제공한다. 

 

코드로 컴포넌트 스캔과 의존관계 자동 주입을 알아보자.

 

먼저 기존 AppConfig.java 는 과거 코드와 테스트를 유지하기 위해 남겨두고, 새로운 AutoAppConfig.java 를 만들자.

컴포넌트 스캔을 사용하려면 먼저 @ComponentScan 을 설정 정보에 붙여주면 된다.

기존의 AppConfig와는 다르게 @Bean으로 등록한 클래스가 하나도 없다!

참고: 컴포넌트 스캔을 사용하면 @Configuration 이 붙은 설정 정보도 자동으로 등록되기 때문에, AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되고, 실행되어 버린다. 그래서 excludeFilters 를 이용해서 설정정보는 컴포넌트 스캔 대상에서 제외했다. 보통 설정 정보를 컴포넌트 스캔 대상에서 제외하지는 않지만, 기존 예제 코드를 최대한 남기고 유지하기 위해서 이 방법을 선택했다.

컴포넌트 스캔은 이름 그대로 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.

참고: @Configuration 이 컴포넌트 스캔의 대상이 된 이유도 @Configuration 소스코드를 열어보면 @Component 애노테이션이 붙어있기 때문이다.

이제 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component 애노테이션을 붙여주자.

 

MemoryMemberRepository @Component 추가

@Component
public class MemoryMemberRepository implements MemberRepository {}

RateDiscountPolicy @Component 추가

@Component
public class RateDiscountPolicy implements DiscountPolicy {}

OrderServiceImpl @Component, @Autowired 추가

@Component
  public class OrderServiceImpl implements OrderService {
      private final MemberRepository memberRepository;
      private final DiscountPolicy discountPolicy;
@Autowired
      public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
  discountPolicy) {
          this.memberRepository = memberRepository;
          this.discountPolicy = discountPolicy;
      }
}

 

@Autowired 를 사용하면 생성자에서 여러 의존관계도 한번에 주입받을 수 있다.

 

AutoAppConfigTest.java

  package hello.core.scan;
  import hello.core.AutoAppConfig;
  import hello.core.member.MemberService;
  import org.junit.jupiter.api.Test;
  import org.springframework.context.ApplicationContext;
  import
  org.springframework.context.annotation.AnnotationConfigApplicationContext;
  import static org.assertj.core.api.Assertions.*;
  public class AutoAppConfigTest {
      @Test
      void basicScan() {
          ApplicationContext ac = new
  AnnotationConfigApplicationContext(AutoAppConfig.class);
          MemberService memberService = ac.getBean(MemberService.class);
          assertThat(memberService).isInstanceOf(MemberService.class);
      }
}

 

AnnotationConfigApplicationContext 를 사용하는 것은 기존과 동일하다. 설정 정보로 AutoAppConfig 클래스를 넘겨준다.실행해보면 기존과 같이 잘 동작하는 것을 확인할 수 있다.

 

컴포넌트 스캔과 자동 의존관계 주입이 어떻게 동작하는지 그림으로 알아보자.

1. @ComponentScan

@ComponentScan @Component 가 붙은 모든 클래스를 스프링 빈으로 등록한다. 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.

빈 이름 기본 전략: MemberServiceImpl 클래스 ➡️ memberServiceImpl

빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶으면 @Component("memberService2") 이런식으로 이름을 부여하면 된다.

 

2. @Autowired 의존관계 자동 주입

생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다. 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.

getBean(MemberRepository.class) 와 동일하다고 이해하면 된다. 더 자세한 내용은 뒤에서 설명한다.

생성자에 파라미터가 많아도 다 찾아서 자동으로 주입한다.

728x90

@Configuration 을 알아보기 위해서 이 코드를 살펴보자,

시나리오

1. @Bean memberService 를 호출하면 ➡️ MemoryMemberRepository() 를 한번 호출한다. 

2. @Bean orderService 를 호출하면 ➡️ MemoryMemberRepository() , discountPolicy() 를 호출한다.

 

이렇게 하면 결과적으로 각각 다른 2개의 MemoryMemberRepository() 가 두 번 호출 되니까 싱글톤이 깨지는게 아닐까요?? 스프링 컨테이너가 싱글톤을 보장해준다고 했는데 과연 그럴까?? 직접 테스트 해보자

 

✅ 테스트 방법

1. MemoryMemberRepository 를 불러오는 MemberServiceImpl 에서 

테스트 용도로 MemberServiceImpl, OrderServiceImpl 둘 다 각각 생성자로 들어오는 memberRepository 의 값을 확인하는 getMemberRepository 메서드를 만들었다.

 

2. 각각의 memberRepository 를 테스트 코드를 통해 확인해보자.

밑에는 테스트코드이고

밑에는 테스트 코드의 결과이다.

결과는 세개의 memberRepository가 같은 것을 확인할 수 있다. 분명 세번의 다른 memberRepository의 인스턴스가 호출될 것 같았지만 모두 같은 인스턴스가 공유되어 사용되고 있음을 확인할 수 있었다. 어떻게 된 일일까? 혹시 두 번 호출이 안되는 것일까? 다음 실험을 통해 알아보자. 

 

이렇게 호출 되는 부분마다 찍어보면서 확인해보았다. 

찍어본 세 개의 메서드 각각 한번씩 호출 되었는 것을 볼 수 있다. 이 비밀이 어디에 있을까?? ➡️ @Configuration 을 적용한 AppConfig 에 있다.

 

시나리오

1. AppConfig 스프링 빈을 조회해서 클래스 정보를 출력해보자.

순수한 클래스라면 다음과 같이 출력되어야 한다.

class hello.core.AppConfig

그런데 예상과는 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다. 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB 이라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.

그 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다. 아마도 다음과 같이 바이트 코드를 조작해서 작성되어 있을 것이다. (실제로는 CGLIB의 내부 기술은 매우 복잡하다.)

 

  • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
  • 덕분에 싱글톤이 보장되는 것이다.

 

만약 @Configuration 을 적용하지 않고, @Bean 만 적용하면 어떻게 될까?

@Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만, 만약 @Bean만 적용하면 어떻게 될까?

 

@Configuration 을 주석처리 하고 @Bean 만 적용해보고 밑에 테스트 코드를 다시 돌려보았다.

결과로 이렇게 나왔다. 

 

CGLIB 기술을 사용 하지 않은 순수한 AppConfig 가 나왔다. 그리고

memberRepository가 여러번 호출되는 것을 확인할 수 있듯이 순수한 자바 코드가 나오면서 싱글톤이 깨진다!!

 

 

정리

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
    • memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
  • 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자.

 

728x90

싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.

그러면 무상태(stateless)로 설계해야 한다!

무상태란.
  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
  • 가급적 읽기만 가능해야 한다.
  • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!!

 

상태를 유지할 경우 발생하는 문제점 예시

최대한 단순히 설명하기 위해, 실제 쓰레드는 사용하지 않았다.

 

  • ThreadA가 사용자A 코드를 호출하고 ThreadB가 사용자B 코드를 호출한다 가정하자.
  • StatefulService 의 price 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.
  • 사용자A의 주문금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나왔다.
  • 실무에서 이런 경우를 종종 보인다고 하는데, 이로인해 정말 해결하기 어려운 큰 문제들이 터진다고 한다.(몇년에 한번씩 꼭 만난다고 한다.)
  • 진짜 공유필드는 조심해야 한다! 스프링 빈은 항상 무상태(stateless)로 설계하자.

 

728x90

싱글톤 컨테이너

= 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성) 으로 관리한다. 
= 스프링 빈(Bean)이 바로 싱글톤으로 관리되는 빈이다.

 

1. 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.

  • 이전에 설명한 컨테이너 생성 과정을 자세히 보자. 컨테이너는 객체를 하나만 생성해서 관리한다.

  • 위 그림에서 보면 빈 객체를 미리 스프링 컨테이너(싱글톤 컨테이너)가 등록해서 관리해준다. 그리고 조회하면 관리되고 있는 객체를 불러온다. 여러개 조회해도 이미 관리되고 있는 객체를 조회한다. 예를 들어 memberService 를 몇번을 불러도 MemberServiceImpl@x01 를 조회해준다.

2. 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.

3. 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.

  • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • 싱글톤 패턴을 위해서 들어가는 지저분한 코드들을 안들어가도 된다. 그리고 " (구체 클래스).getInstance() " 이렇게 해야하기 때문에 DIP 에 위반한다. 스프링이 관리 해주기 때문에 이런 코드가 다 빠져도 된다.
  • DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다. ➡️ 스프링이 다 관리 해주는 덕분이다.

 

이 테스트로 같은 객체를 불러오는 것을 확인할 수 있다.

조회할 때 마다 스프링이 처음에 빈 등록한 것을 계속 반환해주는 것을 확인할 수 있다. ➡️ 싱글톤 맞다~

위 테스트에서 MemberServiceImpl 객체를 불렀는데 이 객체의 코드를 확인해보자. 과연 싱글톤 관련된 코드가 있을까? ⬇️⬇️⬇️

 

싱글톤 관련된 코드는 없는 것을 확인할 수 있다.

 

 

싱글톤 적용 Before After

  • Before

 

  • After

스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.

참고: 스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다. 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다. 자세한 내용은 뒤에 빈 스코프에서 설명하겠다.

 

이러한 싱글톤 방식에도 주의점이 있다.(매우 중요) 다음 글에서 확인해보자.

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

 

[Spring] 싱글톤 패턴 주의점

싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글

ydontustudy.tistory.com

 

728x90

클라이언트가 요청할 때 마다 새로운 인스턴스가 생긴다. 이것은 서버에 많은 비용을 요구한다. 정말로 클라이언트의 요청마다 새로운 인스턴스가 생성이 되는지 확인을 해보겠다.

 

참조값을 확인하면 서로 다른 인스턴스인 것을 알 수 있다.

  • 우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다. -➡️ 메모리 낭비가 심하다.
  • 💡해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. ➡️ 싱글톤 패턴

 

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
  • 그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
    • private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.

 

  1. static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
  2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
  3. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.

  1. private으로 new 키워드를 막아두었다.
  2. 호출할 때 마다 같은 객체 인스턴스를 반환하는 것을 확인할 수 있다.

Singleton 을 적용한 후 이제는 새로운 인스턴스를 만들지 않는다는 것을 확인할 수 있다.

 

💡참고: 싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택했다.

싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다. 하지만 싱글톤 패턴은 다음과 같은 수 많은 문제점들을 가지고 있다.

 

싱글톤 패턴 문제점

싱글톤은 생성된 인스턴스를 공유를 할 수 있고 확실한 객체 하나가 있다는 것이 보장이 되지만 수많은 단점이 있다.

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. ➡️ DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다. (싱글톤은 지정해서 가져오고 미리 인스턴스를 받아놓아서 설정이 끝나버리기 때문에 유연한 테스트가 어렵다.)
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다. (DI 를 적용하기 어렵다.)
  • 안티패턴으로 불리기도 한다.

이러한 단점을 스프링 프레임워크가 전부 다 해결해준다. ➡️ 객체를 싱글톤으로 관리해준다. 

 

다음은 스프링 컨테이너(싱글톤 컨테이너) 의 역할에 대해서 알아보자.

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
 

[Spring] 싱글톤 컨테이너

싱글톤 컨테이너 = 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성) 으로 관리한다. = 스프링 빈(Bean)이 바로 싱글톤으로 관리되는 빈이다. 1. 스프링

ydontustudy.tistory.com

 

 

728x90

1. 스프링 컨테이너 생성

  • new AnnotationConfigApplicationContext(AppConfig.class)
  • 스프링 컨테이너를 생성할 때는 구성 정보를 지정해주어야 한다.
  • 여기서는 AppConfig.class 를 구성 정보로 지정했다.

2. 스프링 빈 등록

  • 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.

빈(Bean) 이름

  • 빈 이름은 메서드 이름을 사용한다.
  • 빈 이름을 직접 부여할 수 도 있다.
    • Bean(name="memberService2")
    • 주의: 빈 이름은 항상 다른 이름을 부여 해야 한다. 같은 이름을 부여하면, 다른 빈이 무시되거나, 기존 빈을 덮어버리거나 설정에 따라 오류가 발생한다.

3. 스프링 빈 의존관계 설정 - 준비

4. 스프링 빈 의존관계 설정 - 완료

  • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)한다.
  • 단순히 자바 코드를 호출하는 것 같지만, 차이가 있다. 이 차이는 뒤에 싱글톤 컨테이너에서 설명한다.
참고: 스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있다. 그런데 이렇게 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다. 여기서는 이해를 돕기 위해 개념적으로 나누어 설명했다. 자세한 내용은 의존관계 자동 주입에서 다시 설명하겠다.

 

728x90

AppConfig

@Configuration
public class AppConfig {// 모든 객체의 생성과 연결을 담당한다. (이렇게 하므로써 DIP 가 완성된다.)

@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
    }

    @Bean
public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
return new RateDiscountPolicy();
    }

    @Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
    }

    @Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

에서 

@Configuration
public classAppConfig {// 모든 객체의 생성과 연결을 담당한다. (이렇게 하므로써 DIP 가 완성된다.)

@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
    }

    @Bean
public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
return new RateDiscountPolicy();
    }

    @Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
    }

    @Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

로 바뀌었다.

 

MemberApp

public class MemberApp {

public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        Member member =newMember(1L,"memberA", 25L, Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = "+ member.getName());
        System.out.println("find member = "+ findMember.getName());
        System.out.println("member = "+ member);
    }
}

에서

public class MemberApp {

public static void main(String[] args) {
        ApplicationContext applicationContext =newAnnotationConfigApplicationContext(AppConfig.class);// @Bean 들을 관리해준다.
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);// ( AppConfig 안에 있는 메서드 이름, 타입 )

        Member member =new Member(1L,"memberA", 25L, Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = "+ member.getName());
        System.out.println("find member = "+ findMember.getName());
        System.out.println("member = "+ member);
    }
}

로 바뀌었다.

 

실행

이렇게 저장 되기 때문에 꺼낼 때 밑에처럼 타입에 이름을 주고 꺼내면 된다.

 

스프링 컨테이너

  • ApplicationContext 를 스프링 컨테이너라 한다.
  • 기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService , orderService )
  • 이전에는 개발자가 필요한 객체를 AppConfig 를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
  • 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.
  • 코드가 약간 더 복잡해진 것 같은데, 스프링 컨테이너를 사용하면 어떤 장점이 있을까? ⇒ 어마어마한 장점이 있다. 차근차근 알아보자

 

스프링 컨테이너가 생성되는 자세한 과정은 다음 글을 참고하면 된다.

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

 

[Spring] 스프링 컨테이너 생성 과정

1. 스프링 컨테이너 생성 new AnnotationConfigApplicationContext(AppConfig.class) 스프링 컨테이너를 생성할 때는 구성 정보를 지정해주어야 한다. 여기서는 AppConfig.class 를 구성 정보로 지정했다. 2. 스..

ydontustudy.tistory.com

 

728x90

SRP: 단일 책임 원칙 (single responsibility principle)

OCP: 개방-폐쇄 원칙 (Open/closed principle)

LSP: 리스코프 치환 원칙 (Liskov substitution principle)

ISP: 인터페이스 분리 원칙 (Interface segregation principle)

DIP: 의존관계 역전 원칙 (Dependency inversion principle)

 

1. SRP 단일 책임 원칙 (single responsibility principle)

하나의 클래스는 하나의 책임만 가져야 한다.

  • 하나의 책임이라는 것은 모호하다.
    • 클 수 있고, 작을 수 있다.
    • 문맥과 상황에 따라 다르다.
  • 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것
    • 예) UI 변경, 객체의 생성과 사용을 분리

책임이라는 것이 실무에 가보면 모호하다. 어떻게 해야 설계를 잘 하는 것일까를 따져보면 중요한 판단의 기준은 변경 이다. 변경을 했을 때 파급(=하나의 클래스나 하나의 지점만 고치는 것)이 적으면 단일 책임 원칙(SRP)를 잘 따르게 설계 한 것이 된다. 

 

2. OCP 개방-폐쇄 원칙  (Open/closed principle)

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

  • 확장을 하려면 당연히 기존 코드를 변경해야 하는거 아냐? => 다형성을 활용하면 가능하다.
  • 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현
  • 역할과 구현의 분리를 생각해보자

 

OCP에 문제점이 있다.

이 그림에 의거해서

이렇게 MemberService 클라이언트가 구현 클래스( 여기에서는 MemoryMemberRepository, JdbcMemberRepository) 를 직접 선택했을 때, 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.

이를 보고 알 수 있듯이, 분명 다형성을 사용했지만 OCP 원칙을 지킬 수 없다.

이 문제를 어떻게 해야하나? => 객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다.  => 결론적으로 별도의 조립, 설정자들이 스프링 컨테이너가 해주는 역할이다. (DI, IoC 컨테이너) 답은 정리에 나와있다.

 

3. LSP 리스코프 치환 원칙  (Liskov substitution principle)

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현체는 믿고 사용하려면, 이 원칙이 필요하다.
  • 단순히 컴파일에 성공하는 것을 넘어서는 이야기

정리 하자면, 자동차 인터페이스가 있으면 구현체가 구현을 하면 되는 기능들이 있다. "악셀" 이라는 기능이 있으면 무조건 앞으로만 가는 법이 없으니 뒤로 가는 기능을 내가 만들었다고 쳐도 컴파일은 성공적으로 될 것이다. 리스코프 치환 원칙은 컴파일 단계를 단순히 얘기하는 것이 아니다. 인터페이스 규약이 "악셀"은 무조건 앞으로 가야 한다는 규약이 있을 것이다. 이런 규약을 맞춰야 하는 것이 이 원칙이다. (기능적으로 보장을 해줘야 한다)

 

4. ISP 인터페이스 분리 원칙  (Interface segregation principle)

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
    • 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
    • 사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트로 분리
  • 분리하면 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음
  • 인터페이스가 명확해지고, 대체 가능성이 높아진다.

운전 인터페이스와 정비 인터페이스로 분리 하면 클라이언트를 운전자 클라이언트와 정비사 클라이언트로 분리 할 수 있다. 예를 들어 정비에 관련된 문제가 있어서 기능을 바꿔야할 때 정비사 클라이언트에 대한 부분만 바꾸면 된다. 결국에는 기능에 맞게 잘 인터페이스를 쪼개는게 좋다는 뜻이다. 

 

5. DIP 의존관계 역전 원칙  (Dependency inversion principle)

  • 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나이다.
  • 쉽게 이야기해서 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻
  • 앞에서 이야기한 역할(Role)에 의존하게 해야 한다는 것과 같다. 객체 세상도 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다. 구현체에 의존하게 되면 변경이 아주 어려워진다.

클라이언트 코드가 구현 클래스를 바라보지 말고 인터페이스만 바라봐라는 뜻 예를 들어 MemberService 가 MemberRepository 인터페이스만 바라보고 구현 클래스 ( MemoryMemberRepository, JdbcMemberRepository )를 바라보지 말라는 얘기이다. 

 

여기서도 예를 들어 설명하면, 운전자는 자동차 역할 만 알면 되지 K3, 아반떼, 테슬라 모델3 를 알 필요가 없다는 뜻 DIP 이다.

 

  • 위의 OCP에서 설명한 MemberService는 MemberRepository인터페이스에 의존하지만, 구현 클래스에도 동시에 의존한다.
  • MemberService 클라이언트가 구현 클래스를 직접 선택
    • MemberRepository m = new MemoryMemberRepository();

MemberService 는 인터페이스인 MemberRepository 를 의존하고 있지만 구현 클래스인 MemoryMemberRepository 도 의존하고 있다. 즉 DIP를 위반하고 있다. 그러면 어떻게 DIP를 지킬 수 있을까? 답은 정리에 나와있다.

 

 

정리

  • 객체 지향의 핵심은 다형성
  • 다형성 만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없다.
  • 다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경된다.
  • 다형성 만으로는 OCP, DIP를 지킬 수 없다.
  • 뭔가 더 필요하다.
    • 스프링은 다음 기술로 다형성 + OCP, DIP 를 가능하게 지원
      • DI(Dependency Injection): 의존관계, 의존성 주입
      • DI 컨테이너 제공
    • 클라이언트 코드의 변경 없이 기능 확장
    • 쉽게 부품을 교체하듯이 개발

 

권장사항 정리

  • 모든 설계에 열할 구현을 분리하자
  • 애플리케이션 설계도 공연을 설계하듯이 배역만 만들어두고, 배우는 언제든지 유연하게 변경할 수 있도록 만드는것이 좋은 객체지향 설계이다
  • 이상적으로는 모든 설계에 인터페이스를 부여하자

 

고민

  • 인터페이스를 남발하면 추상화라는 비용이 발생한다. 
  • 기능을 확장할 가능성이 없다면, 구현 클래스를 직접 사용하고, 향후 꼭 필요할 때 리팩토링해서 인터페이스를 도입하는 것도 방법이다.

 

728x90

스프링을 배우기 전에 알아둬야 할 제일 중요한 것은 "이 프레임 워크를 왜 만들었을까?"라고 해도 과언이 아니다. 우리가 흔히 웹 프레임 워크를 공부할 때 현실적인 목적은 웹 애플리케이션 만들고, DB 접근 편리하게 해주는 기술이고 웹 서버도 자동으로 띄어주기도 하고 특히 자바 스프링은 전자정부 프레임워크 이기도 해서 스프링의 핵심이 될 수 있을까? 이러한 것들은 단지 결과물일 뿐이고 진짜 핵심은 따로 있다.

 

"스프링은 자바 언어 기반의 프레임워크이다."  에서 진짜 스프링의 핵심이 있다. 그 이유는 자바 언어가 객체지향 언어이기 때문이다. 즉 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내도록 하는 프레임워크이다. 그리하여 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크이다.

 

따라서, 좋은 객체 지향이 무엇인지 논해볼 필요가 있다. 먼저 객체 지향 프로그래밍의 정의는 다음과 같다. 

  • 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위, 즉"객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다.(협력)
  • 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.

여기서 유연하고 변경이 용이하다는 것은 무엇일까 생각해 볼 수 있다. 예를 들어 레고 블럭 조립 하듯, 키보드 마우스 갈아끼우듯, 컴퓨터 부품 갈아 끼우듯이 쉽게 끼워맞추고 변경할 수 있는 것으로 생각해볼 수 있고. 조금 더 개발 언어로 말을 해보면 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 것이라고 생각할 수 있다. 이러한 유연함은 "다형성(Polymorphism)"에서 나온다.

 

다형성(Polymorphism)

위와 같은 예시에서 볼 수 있는 것은 다음과 같다. 운전자의 자동차가 K3 에서 아반떼로 바뀌었는데 운전자는 운전에 영향을 받을까? 그렇지 않다. 즉 자동차가 바뀌어도 운전자에게 영향을 주지 않는다. 이것이 가능한 이유는 모든 자동차는 자동차 라는 인터페이스에 따라서 자동차를 구현했기 때문이다. 운전자는 자동차의 역할(interface)에만 의존하고 있는중이다. 

자동차라는 역할을 만들고 구현을 분리한 이유는 무엇일까? 운전자를 위해서이다. ( 개발적으로 봤을 때 여기서의 운전자랑 클라이언트(client) 는 맥락상 비슷한 의미를 가진다. ) 운전자들은 자동차의 내부 구조를 몰라도 된다. 즉 자동차가 내부적으로 바뀌더라도 자동차의 역할만 제대로 하면 운전자에게 영향을 안준다. 이것은 역할구현 으로 세상을 구분했기 때문에 가능한 일이다. 제일 중요한 것은 자동차를 여러개 구현할 수 있는 것이 중요한게 아니라 새로운 자동차가 나와도 운전자들은 새로운 것을 배우지 않아도된다는 것이다.

클라이언트를 변경하지 않고 서버의 구현 기능을 유연하게 변경할 수 있다.
- 다형성의 본질

다형성의 본질을 이해하려면 협력이라는 객체사이의 관계에서 시작해야한다. 

 

정리하자면,

역할과 구현을 분리

정리
  • 실세계의 역할과 구현이라는 편리한 컨셉을 다형성을 통해 객체 세상으로 가져올 수 있음
  • 유연하고, 변경이 용이
  • 확장 가능한 설계
  • 클라이언트에 영향을 주지 않는 변경 가능
  • 인터페이스를 안정적으로 잘 설계하는 것이 중요
한계
  • 역할(인터페이스) 자체가 변하면, 클라이언트, 서버 모두에 큰 변경이 발생한다.
    • 예시로
      • 자동차를 비행기로 변경해야 한다면?
      • 대본 자체가 변경된다면?
      • USB 인터페이스가 변경된다면?
  • 인터페이스를 안정적으로 잘 설계하는 것이 중요하다

 

지금까지 다형성에 대해서 얘기해 왔다. 그 이유는 스프링의 꽃은 다형성이라고 말해도 무방할 정도이기 때문이다. 스프링은 다형성을 극대화해서 이용할 수 있도록 도와준다. 무엇으로 도와주느냐면, 스프링은 제어의 역전(IoC), 의존관계 주입(DI)로 역할과 구현을 편리하게 다룰 수 있도록 지원한다.

728x90

 

이제 Hadoop의 MapReduce는 어떻게 되어있을까 살펴보자.

 

MapReduce와 HDFS가 어떤식으로 결합해서 실행되는지 살펴보자. 특히 Hadoop 1.xx 버전에서는 이렇게 실행된다. 앞에 구글 MapReduce에서 master라고 했던 부분은 JobTracker라는 master processor로 Hadoop에서 실행되고, 구글 MapReduce의 Worker는 Hadoop에서 TaskTracker라는 slave processor로 실행된다. 기억을 더듬어 보면 HDFS에서 namenode라고 하는 master가 있어서 얘가 메타데이터(meta data)를 관리했고 datanode가 각각의 slave node에서 실행되면서 local Linux file system에 실제 애플리케이션 데이터를 관리하는 형태로 실행됐었다. 비슷하게 MapReduce가 실행되기 위해서는 Master processor인 JobTracker, 그러니까 job을 제출하는 job submission node에서 JobTracker가 실행돼야하고 각각의 slave node에서 tasktracker보다는 slave process가 실행되어야 한다. 그러니까 namenode, datanode가 hdfs에서 cluster를 구성하는 것이고 JobTracker와 tasktracker가 MapReduce 프레임워크를 구성하고 있다고 보면 된다. namenode와 job submission node를 하나로 묶는 경우도 있고 기계가 남아돌면 분리 하는 경우도 있다. 

Slave node를 물리적인 노드를 의미한다고 생각하면 된다. 각각의 물리노드에 hdfs를 위한 datanode daemon과 MapReduce를 위한 tasktracker를 모두 실행되고 있는 것을 볼 수 있다. 왜 이렇게 실행 되어야 하냐면 만약 둘 중 하나가 빠져있으면 문제가 생긴다. 정확히 말하면 MapReduce를 처리할 때 문제가 생길 수 있는데, 예를 들어서 한 slave node에 tasktracker가 없다고 치고, datanode는 다 돌고 있다고 하자. 그러면 job submission node에 있는 JobTracker가 tasktracker가 없는 노드로는 map task나 reduce task를 보낼 수 없다. Task tracker가 실제로 JobTracker의 명령을 받아서 map 또는 reduce task를 실행하는 주체이기 때문에 tasktracker가 없다고 하는것은 task를 실행할 수 없는 것이기 때문에 Mapeduce 프레임워크 입장에서 봤을때는 해당 slave node가 없는 것과 같은 맥락이다. 

반대로 tasktracker가 있는데 datanode가 안돌고 있다고 하면 어떻게 될까? 그렇게 되면 해당 slave node는 namenode에게 눈에 보이지 않는 것이다. 즉 해당 slave node는 없는 것과 마찬가지이다. 그래서 datanode와 tasktracker는 항상 같은 노드에서 실행이 되어야 한다. 

 

ps. Hadoop yarn에서도 같은 맥락에서 동작한다.

 

Hadoop job processing step

MapReduce job processing의 step을 그림으로 살펴보자.

 

  1. Run job : client가 job을 실행해 달라고 JobClient 클래스에 요청을 한다.
  2. Get new job ID : 새로운 job ID를 JobTracker에게 요청해서 job ID를 받게된다. 
  3. Copy job resources : job에 관련된 resource들을 shared file system에 업로드한다. (Job resource라고 하는 것은 보통은 mapper와 reduce 클래스 다 포함하고 있는 jar 파일 같은 것이다. Jar 파일 같은것들을 hdfs에 올려놓아야 나중에 MapTask나 ReduceTask를 실행할 수 있게 된다. 왜냐하면 MapReduce job을 submission 하면은 실제 Map과 Reduce가 어디서 실행 되냐면은 전적으로 JobTracker가 알아서 한다. 이 얘기는 MapReduce job의 map task와 reduce task가 Hadoop cluster를 구성하는 어느 node에서 실행될지 모른다. 사전에 알면 그 노드에 필요한 파일들을 갖다놓을것이다. 그러나 안된다. 어디서나 접근이 가능한, 모든 노드에서 접근을 할 수 있는 file system이 대표적으로 hdfs이기 때문이다.)
  4. Submit job : job을 제출한다.
  5. Initialize job : JobTracker가 initialize 한다.
  6. retrieve input splits : input split은 MapReduce job이 처리하고싶은 단위이다. 그러니까 64mb씩 쪼개서 각각의 input split에 대해서 어느 tasktracker에 있는지 찾아내야한다. 그래야 각각에 대한 input split에 mapper를 띄울 때 최대한 그 데이터에 가깝게 스케쥴링 할 수 있기 때문이다.
  7. Heartbeat : input split을 기반으로 해서 tasktracker에게 작업을 할당하게 되면 주기적으로 heartbeat을 보내게된다. 
  8. Retrieve job : tasktracker는 child JVM을 launch시켜서 Map 또는 Reduce task를 실행하게 되는데 그 전에 해야할게 HDFS에 접근해서 job resource(jar 파일 같은거)를 받아와야한다. jar파일 같은게 로컬하게 있어야 map/reduce task를 실행시킬수있다. 
  9. Launch : tasktracker가 childJVM을 launch시키고
  10. Run : 사용자가 정의하는 map/reduce함수를 적용하는 과정이다.

 

정리하면, 기본적으로 사용자가 개발한 MapReduce 프로그램 실행파일 jar 있어야 하고, jar파일이 hdfs 올라가 있고 처리에 필요한 input data hdfs 있다라고 가정을 하는 것이다. 그런 과정에서 JobTracker 각각의 input split 대해서 hdfs namenode에게 물어봐서 각각이 어디에 있는지 찾은 다음에 거기 최대한 가깝게 map task 실행한다. 실제 실행이라고 하는 것은 tasktracker childJVM 통해서 사용자가 정의한 map/reduce 함수를 적용하는 과정이다.

 

 

JobTracker

JobTracker를 자세히 살펴보도록 하겠다.

 

JobTracker가 하는 가장 중요한 일은 기본적으로 cluster에 있는 특정 노드들의 MapReduce task들을 경작(farm out) 하는 것이다. (task는 job을 이루는 구성요소) (MapReduce task는 map task + reduce task를 말한다) input data를 갖고있는 그 노드에 map task를 가져다 주거나 적어도 같은 rack에 있는 노드에서 map task를 실행해서 같은 rack에 있는 데이터 노드를 복사해올 수 있게한다. 이것은 data locality에 해당된다. 

Data locality를 구현하는 주체는 JobTracker이다. 

그런데 JobTracker라고 하는 것은 single point of failure이다. (단일고장점) 만약 JobTracker가 죽게되면 모든 실행중인 MapReduce job은 멈추게(halting) 될 수 밖에 없다.

많은 사람들의 job을 관리해야 하니까 JobTracker가 가지는 overhead는 클 수 밖에 없다. 수많은 task에서 state정보를 메모리에서 계속 관리하고 있기 때문에 너무 자주업데이트 된다. 회사로 예를 들면 부서별로 JobTracker가 따로돌면 관리부담도 줄어들고 좋다. 이런 부담을 도입해서 Hadoop2에서는 yarn이라는 운영체제가 나오면서 apllication master라는 대체개념이 나온다. 

 

MapReduce job processing

  1. Client apllication이 MapReduce job을 JobTracker한테 제출한다. 
  2. 그 이후 JobTracker가 최대한 가깝게 스케쥴링을 해야하니까 기본적으로 input data가 있는 위치를 물어보기 위해 namenode랑 컨택한다. (JobTracker는 프로세싱만 하고 데이터관리는 안한다)
  3. namenode로 부터 받아서 그 노드들 중에 최대한 input data랑 가깝게 있는 tasktracker node를 찾는데 (가용한 slot이 있는 경우)
  4. TaskTracker를 찾으면 TaskTracker node들 한테 일을준다.
  5. TaskTracker node들이 기본적으로 모니터링된다. Data node와 namenode처럼 heartbeat을 주고받는다. (분산시스템에서 서로의 생사를 확인할 수 있는 방법은 구글처럼 master가 ping을 하던지, 하둡에서 slave가 주기적으로 heartbeat을 보내주던지 이 두개밖에 없다) 만약에 heartbeat을 받지 않으면 다른 그 작업을 다른 tasktracker한테 스케쥴링 하게 된다.
  6. 자기가 실행하고 있는 task가 에러가 나면서 죽으면 JobTracker에게 알려준다. Jobtracker가 fail난 task를 다시 다른곳에 resubmission 한다. 이것은 혹시라도 그 tasktracker가 실행되는 node의 환경이 맞지 않아서 task가 죽은 것일수도 있으니까 다른곳에서도 해보는 것이다.(계속 죽으면 블랙리스트에 넣는다)
  7. 전체적인 작업이 끝났다. 이 얘기는 MapReduce작업이 끝났다는 얘기이다.
  8. 그러면 client apllication이 JobTracker한테 관련된 정보를 가져올 수 있게되는 형태로 진행된다.

 

MapReduce의 progress 기본적으로 JobTracker 담당한다고 보면 된다.

 

 

TaskTracker

TaskTracker라고 하는 것은 기본적으로 map, reduce, shuffling과정이 있었는데 이러한 task들을 jobtracker한테 받아서 실행하는 역할을 수행한다. 그런데 shuffling task라는 것은 reduce task가 remote procedure call을 통해서 map task가 실행됐던 그 node의 데이터를 복사해 가는거니까 조금 다른데 map과 reduce같은 경우에는 slot이 정해져 있다. 각각의 TaskTracker 같은 경우는 slot이 정적으로 설정되어 있다. 동적으로 실행시간이 변하는게 아니다. 각각의 TaskTracker가 받아들일 수 있는 mapper 또는 reduce의 개수가 사전에 구성이 되어있다 라고 이해하면된다. Static configuration의 문제는 뭐냐면 결과적으로 Hadoop cluster가 homogenous하다고 가정하는 것이다. node의 하드웨어 스펙이 비슷하니까 각자 균등하게 분할해서 똑같은 숫자의 Mapper와 똑같은 숫자의 reduce를 처리하면 되겠네 라고 하는 naive한 방식으로 시작한 것이다. hadoop이 탄생할 때 이런 모습이고 목적이었는데, 시간이 지날수록 하드웨어 자원이 발전하니까 각각의 노드의 컴퓨팅 파워가 달라지기 시작하고, 문제가 뭐냐면 hadoop 생태계가 커지면서 MapReduce말고도 다른 형태의 프레임워크가 들어오기 시작했다. 이런것들 때문에 고정적인 slot기반의 자원관리가 좋지 않다. (slot의 단점)

 

각각의 TaskTracker는 separate JVM process를 낳아서(spawn해서) 실제 일(actual work)을 한다. Separate JVM을 launch하는 이유는 map이나 reduce task가 실행되다가 죽더라도 tasktacker자체는 별로 영향을 미치지 않게 하기 위한 것이다. 

그래서 spawning된 프로세서를 TaskTracker는 꾸준히 모니터링 하면서 output과 exit코드를 캡쳐 해서 jobtracker한테 보고하게 된다. 

프로세서가 끝나게 되면은(성공적으로 끝났던, 중간에 에러나서 죽었던) 이 결과를 기본적으로 jobtracker한테 보고(notification)하고 다음 지침을 받는다. 그리고 중요한 것은 각각의 TaskTracker들은 jobtracker한테 주기적으로 heartbeat message를 보낸다. (TaskTracker가 살아있음을 계속 알림)

각각의 TaskTracker가 주기적으로 heartbeat을 보내면서 현재 가용한 slot의 개수를 알려준다. 그러면 jobtracker는 이 cluster를 구성하는 전체 TaskTracker의 가용한 slot들을 다 알고 있는 것이된다. 그러면 새로운 MapReduce job이 들어와서 task assign을 할 때 그 slot들을 보면서 결정하는 것이다. 고려해야 할 것이 두 가지이다. 

번째는 task 실행하려면 slot 있어야 한다. 번째는 data locality 고려해야 한다. jobtracker 최대한 data 가깝게 보내고 싶어한다. 그래야 전체적인 cluster 활용율이 올라가기 때문이다. 

'서버 > 클라우드 컴퓨팅' 카테고리의 다른 글

Hypervisor vs Container  (1) 2024.09.05
MapReduce: Fault Tolerance, Locality, Large-Scale Indexing  (0) 2021.12.16
MapReduce: Programming Model  (0) 2021.12.16
MapReduce  (0) 2021.12.16
빅데이터 Parallelization의 문제점  (0) 2021.11.09

+ Recent posts