TIL - 0530

쓰면서 배우는 스프링(부트)

로그인 관련 기능 테스트

  • 피드백 받은 내용 중 가장 뼈져리게 아픈 부분은 테스트 케이스를 작성하면서 실패 케이스를 깜빡했다는 것
    • 테스트 케이스를 작성할 때에는 성공 케이스보다 실패 케이스가 더 중요함 : 실패 케이스일 때 브라우저가 어떤 응답을 받을지 테스트 꼭 해야함
@Test
public void login_fail_invalid_password() {
    String userId = "javajigi";
    builder.addParameter("userId", userId);
    builder.addParameter("password", "1111");
    HttpEntity<MultiValueMap<String, Object>> request = builder.build();
    ResponseEntity<String> response = template.postForEntity("/users/login", request, String.class);

    assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
}

@Test
public void login_fail_invalid_userId() {
    String userId = "colin";
    builder.addParameter("userId", userId);
    builder.addParameter("password", "1234");
    HttpEntity<MultiValueMap<String, Object>> request = builder.build();
    ResponseEntity<String> response = template.postForEntity("/users/login", request, String.class);

    assertFalse(userRepo.findByUserId(userId).isPresent());
    assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
}
  • 예외 상황일 때 uncheckedException을 발생시켜서 반드시 개발자가 잊지않고 처리하도록 하기
/* controller */
@PostMapping("/login")
public String login(String userId, String password, HttpSession session) throws UnAuthenticationException {
    User user = userService.login(userId, password);
    session.setAttribute(HttpSessionUtils.USER_SESSION_KEY, user);
    return "redirect:/users";
}

/* controllerAdvise */
@ExceptionHandler(UnAuthenticationException.class)
@ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public void unAuthentication() {
    log.debug("UnAuthenticationException is happened!");
}

스프링 내부 동작, 원리

싱글톤, 싱글톤 레지스트리

스프링 컨테이너에서 관리되는 빈들은 대부분 싱글톤임
  • 이외 스코프도 있음, 유지되는 동안 하나의 인스턴스를 가지고 사용

싱글톤 레지스트리

  • 싱글톤 패턴에 따라 같은 인스턴스를 get하는게 아니라, 싱글톤이 유지되도록 관리함
  • 싱글톤 패턴은 생성자가 private 이므로 상속관계를 만들 수 없음 : 객체지향의 한 특징인 상속을 사용할 수 없음
  • 서버 환경에서는 싱글톤이라고 장담할 수 없음

멀티쓰레드 환경에서 싱글톤은 상태를 가지면 안됨

  • 동시에 접근해서 변경할 수 있으므로, 상태 공유 상태가 됨, 그래서 상태값을 가지는 객체는 빈으로 설정하지않음
  • 읽기 전용 상태나 1번 할당되면 변경되지않는 상태값이라면 상관없음 : 안전하게 final로 처리
  • 변경되는 값들은 메소드 파라미터로 받도록 해야함

DI

Dependency Injection
  • 의존관계 주입, A오브젝트가 B오브젝트의 참조(기능 사용을 위해서)할 수 있게 외부로부터 주입 받음
  • A오브젝트가 B오브젝트가 가진 역할이 필요한데, 외부에서 B오브젝트의 레퍼런스를 주입시켜줘서 A오브젝트가 내부적으로 B오브젝트의 기능을 사용하면서 자신의 역할을 수행함
  • 의존 관계를 선언할 때는 약한 결합을 위해 인터페이스, abstract 타입으로 선언해야함 : 구체적인 클래스를 명시하지않음(없어지거나 이름 변경되면 어떡하니?)
    • 단순히 오브젝트를 제공받았다고 해서 DI가 아니라 그떄에 맞는 의존관계를 주입받아야 DI라고 함 : 코드만 분리해둔 것과 결정 권한 역할까지 분할한 차이(다이나믹하게)라고 생각됨, 의존성 주입을 사용하는 장점을 취해야하는데 그 장점(확장, 변경에 유연)을 다 버리고 고정된 오브젝트만 그냥 사용하는 것이므로 내부적으로 구현해도되는데 굳이 코드만 분리해둔거니깐 DI라고 하지않는 것이라 생각됨
  • 컨트롤 권한을 컨테이너에 모두 넘겨줘야함 : @Component 선언으로 스프링 빈으로 등록해야함
    • @Component와 @Bean의 차이 : @Component는 컨테이너에서 관리할 오브젝트에 적용하는 어노테이션(클래스 레벨에만 적용, 자동 스캔 대상), @Bean은 오브젝트 생성을 담당하는 메소드(설정사항 담고 있음 - @Configuration)에 적용하는 어노테이션
DI를 사용하면?
  1. 의존 관계에 있는 타입 중 어떠한 타입의 인스턴스를 주입 받아도 상관없음 : 각각 다르게 동작 시킬 수 있게됨
  2. 사용과 사용하지않음을 알아서 컨트롤 할 수 있음
의존 관계 방향성
  • A오브젝트가 B오브젝트가 가진 역할이 필요해서 의존 관계를 주입받는다면 A가 B를 의존하고 있다고 함 : 반대로 B는 A와 의존관계가 없음
  • B의 변화가 A에 영향을 미침, A의 변화가 B에는 영향 없음 : B의 역할이 완전 바뀌는 그런 영향이 아니라 내부 동작 변경되는 것, 사용하는 A의 입장에서는 아무런 상관없도록 표준 인터페이스 만들어두고 A는 인터페이스에 의존하도록 - B 하위 타입 어떤 타입이 생기더라도 A는 B 타입의 인스턴스만 주입받아서 사용하면 됨
public class UserDao {
    private final ConnectionMaker maker;
    
    public class UserDao(ConnectionMaker maker) {
        this.maker = maker;
    }
}
의존관계 주입하기
  • 잘못된 의존관계 설정 : 특정 클래스의 인스턴스를 의존함, 해당 클래스가 사라진다면? 해당 클래스의 변경이 UserDao에도 영향을 미치게 됨, 물론 주입해주는 것도 아니고 본인이 고정하고 있는 것임
public class UserDao {
    private final ConnectionMaker maker;
    
    public UserDao() {
        maker = new DConnectionMaker();
    }
    
    public void add(User user) {
        Connection conn = maker.make();
    }
}
  • 생성자 의존 관계 주입 : 파라미터를 통해서 ConnectionMaker 타입의 인스턴스를 받되, 그 어떤 의존 오브젝트일지는 결정 권한은 DI 컨테이너에 달려있음
    • 위의 코드처럼 고정적인 의존관계를 만드는게 아니라 상황에 따라서 맞는 의존관계 주입함 : 주입을 위해서는 A, B를 제외한 C의(세번째) 역할이 필요함(DI 컨테이너)
public class UserDao {
    private final ConnectionMaker maker;
    
    public UserDao(ConnectionMaker maker) {
        this.maker = maker;
    }
    
    public void add(User user) {
        Connection conn = maker.make();
    }
}
  • 팩토리를 의존하고 있고, 팩토리에 검색 요청에 의한 의존 관계 주입
    • 굳이 Factory의 존재를 알 필요가 있을까? 의존 관계 주입만 받으면 되는데….?
    • 스프링 컨테이너(Application Context)도 getBean() 검색 후 의존 관계 주입을 받을 수 있음
public class UserDao {
   private DaoFactory factory = DaoFactory.of():
   private ConnectionMaker maker;
   
   public UserDao() {
       maker = factory.getConnectionMaker();
   }
}

/* Application Context */
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
maker = context.getBean('connectionMaker', ConnectionMaker.class);

reference

네트워크

패킷은 어떻게 목적지까지 찾아갈까?

  • 패킷이 전달될 지점에 대한 정보를 부과하는 프로토콜 스택 내 IP 담당 부분(TCP 담당까지 TCP 헤더 + 데이터로 구성된 패킷이 만들어짐)
    • 해당 패킷에 어디로 가야할지에 대한 제어정보를 부과함 : 헤더에 부과함 - 이더넷 / IP 헤더
    • IP 헤더의 IP 주소는 최종 목적지(서버)로 고정되어있고, 이더넷(MAC 주소) 헤더에는 다음 라우터의 MAC 주소가 적힘 : 바로 찾아가는게 아니라 중계되면서 최종 목적지를 찾아감(라우터, 허브), 처음 라우터의 주소는 가장 가깝고 같은 방향 라우터의 주소
  • 정보 부과가 끝나면 LAN 어댑터에 보내서 LAN 어댑터는 받은 디지털 데이터를 전기 신호로 바꿔 송신함, 수신했을 때에는 전기 신호를 디지털 데이터로 변경해서 송신할 때 거쳤던 단계를 거꾸로 거침
    • 각각의 네트워크 계층은 맡은 정보를 부과하고 해석함 : 서로에 대한 터치가 없음(연계되어 있지만 독립적임)