TIL - 0605

테스트 코드 중복 없애기

  • 테스트 코드 중복을 없애기 위해서 2가지 방법(+@)이 있음
    1. @Before 메소드를 사용 : 모든 테스트메소드가 거치기때문에 필요없는 메소드도 호출하는 단점이 있음
    2. 일반 메소드(prehandle 개념 메소드)를 정의해두고 prehandle을 거쳐야하는 @Test에서 호출하는 방법 : 코드양은 늘어나지만 필요한 메소드에만 적용시킬 수 있음
    3. +@ 방법은 2번째에서 상속을 사용해서 중복을 제거하는 것
  • 테스트 코드 또한 중복을 제거하지않으면 테스트 케이스가 늘어날수록 중복 제거한 코드보다 코드양이 훨씬 더 늘어남
  • 2번째 방법으로 중복 제거하기
public class QnaAcceptanceTest extends AcceptanceTest {
    
    private ResponseEntity<String> sendGetRequest(TestRestTemplate template, String reqUrl) {
        return template.getForEntity(reqUrl, String.class);
    }
    
    @Test
    public void read() {
        ResponseEntity<String> response = sendGetRequest(template(), "/questions/1");
        assertThat(response.getStatusCode(), is(HttpStatus.OK));
        log.debug("body : {}", response.getBody());
    }
    
    .
    .
    .
    
    
    @Test
    public void edit_unAuthorize() {
        ResponseEntity<String> response = sendGetRequest(basicAuthTemplate(), "/questions/2/form");
        assertThat(response.getStatusCode(), is(HttpStatus.FORBIDDEN));
    }
}
  • 3번째 방법을 적용한 테스트코드 작성
public abstract class AcceptanceTest {
    .
    .
    .
}

public class QnaAcceptanceTest extends AcceptanceTest {
    .
    .
    .
}
  • 코드 중복은 비즈니스 로직이나 테스트 코드나 모든 코드에서 항상 경계해야하고 제거하려고 해야함! : 항상 클린코드 만들기, 메소드 빼기 -> 상속 -> DI

트랜잭션과 @Transactional

  • 트랜잭션 : 데이터베이스에 저장되어있는 데이터의 상태를 변경하는 작업의 단위, 작업 단위는 직접 정할 수 있음(여러 쿼리 작업을 묶어 하나의 트랜잭션으로 만들 수 있음)
  • 트랜잭션의 흔한 예 : 은행 송금 과정(송금에 필요한 모든 과정 - 트랜잭션)이 모두 완료되었을 때 비로소 commit(작업의 결과를 데이터베이스 동기화 작업)함
  • 트랜잭션의 특성
    1. 원자성 : 트랜잭션으로 묶은 작업들은 더이상 쪼갤 수 없음(원자), 작업 처리 도중 에러가 발생하면 작업 하기 이전의 상태로 rollback, 모두 성공했을 때 commit
    2. 고립성 : 여러 트랜잭션을 처리할 때 서로의 트랜잭션에 대해 간섭할 수 없음, 트랜잭션 부분 상태 제공할 수 없음
    3. 일관성 : 트랜잭션을 처리하는 과정에서 발생한 결과를 가지고 트랜잭션 처리를 하는 것이 아니라 트랜잭션을 시작한 상태에서의 데이터 상태를 가지고 트랜잭션 처리를 함
    4. 지속성 : 트랜잭션 성공 후 반드시 데이터베이스에 동기화시켜야함
  • @Transactional : 스프링에서 제공하는 트랜잭션을 선언하는 어노테이션, 어노테이션을 메소드에 적용시킬 수 있음(Type도 있는데 공부해봐야겠음)
    • 메소드에 해당 어노테이션을 선언해두면 해당 메소드 내에 있는 작업이 모두 성공했을 때 commit
  • 적용해보기
    • JPA/Hibernate 사용 환경
    • 하나의 작업처럼 보이지만 실제 작업(상태 변경)을 처리하는(엔티티 - 도메인)객체가 여러개임 : 질문, 질문의 답변들
    • 객체의 상태를 변경하던 도중 에러가 발생하면 commit 하지않고, rollback : Persistence Context에서 commit 처리를 하지않음
@Service("qnaService")
public class QnaService {
    
    @Autowired
    private QuestionRepository questionRepo;
    
    
    
    @Transactional
    public void delete(User loginUser, Long id) throws CannotDeleteException {
        questionRepo.findById(id).get().delete(loginUser);
    }
}

/* domain(entity) */
@Entity
public class Question extends BaseEntity {

    @ManyToOne
    private User writer;
    
    @OneToMany(mappedBy = "question", cascade = CascadeType.ALL)
    private List<Answer> answers = new ArrayList<>();
    
    public List<DeleteHistory> delete(User loginUser) throws CannotDeleteException {
         .
         .
         .
         .
         deleteAnswers(...);
         return histories;
    }
    
    private void deleteAnswers(List<DeleteHistory> histories) throws CannotDeleteException {
        for (Answer answer : answers) {
            histories.add(answer.delete());
        }
    }
}

JPA Persistence Context

  • 엔티티를 영구적으로 저장할 수 있는 환경
  • 컨텍스트에 들어간다고 해서 영구적으로 저장되는 것은 아님 : 관리상태로 들어가는 것이면서 컨텍스트가 가진 장점을 누릴 수 있음
  • 중간 계층 : 일반 객체 - 데이터베이스 레코드 사이의 중간 계층 역할
    • 중간 계층을 가짐으로서 가지는 여러 장점이 있음
    • 중간 계층을 만들어두고 데이터베이스와의 연결, 데이터 상태 변경 작업 자체에 대한 성능을 다듬을 수 있음
  • @Id를 통해서 객체를 구별함 : @Id없이 관리상태로 만들려고 하면 에러남

  • 엔티티의 라이프사이클(상태)과 영속성 컨텍스트
    • new : 영속성 컨텍스트 관리하지않은 상태, 말그대로 new로 생성되기만한 순수 자바 객체, POJO(어떠한 외부 라이브러리 접촉x)
    • manager : 영속성 컨텍스트에서 관리하는 상태, 컨텍스트가 가진 장점을 누리는 중
    • detached : 컨텍스트에 의해 관리되고 있다가 떨어져 나간 상태, 컨텍스트가 가진 장점을 하나도 누릴 수 없음
    • removed : 삭제된 상태
  • 영속성 컨텍스트의 장점
    • 캐시 가능 : 관리 객체(엔티티)를 내부 Map에 저장(key는 @Id, value는 객체), 같은 @Id에 대한 객체는 데이터베이스 조회없이 캐시된 객체 리턴
    • 같은 식별값(@Id)을 가진 엔티티라면 같은 객체로(동일성) : 새로운 객체를 생성해서 리턴해주는게 아니라 캐시해둔 엔티티 객체를 리턴함(같은 객체일 수 밖에)
    • 변경 추적 : 관리 객체는 수정되면 자동적으로 필드(컬럼, 상태) 전부를 수정해줌, commit() 전 flush() 호출해서 수정된 객체를 수정해주는 작업을 함(on memory - 객체다)
  • 영속성 컨텍스트의 지연 SQL 저장소
    • 트랜잭션 지원
    • 트랜잭션 처리를 할 때 SQL 한꺼번에 데이터베이스에 입력, commit(동기화작업 요청) 1번만 요청하도록

HTTP 인증모델과 인증 프로토콜(1)

  • 특정 요청 URL에 대해서 모든 요청자가 정상적인 응답을 받을 수 있는 것이 아니라 인증을 받아야만 응답 받도록 해야할 때가 있음 : 관리자 권한이 필요한 메뉴 같은…!
  • HTTP 인증 모델(HTTP가 제시하는 표준 방법) : 위와 같은 상황에서 어떻게 인증 흐름을 가져가야하는지 정의해놓은 것
  • HTTP 인증관련 헤더와 흐름
    1. WWW-Authenticate(서버 -> 클라이언트, 인증 요구, 401)
    2. Authorization(클라이언트 -> 서버, 인증 정보, 인증 요청)
    3. Authentication-info(서버 -> 클라이언트, 200, 인증됨, 추가적으로 인증 알고리즘에 대한 정보를 제공할 때 헤더의 value에)
  • HTTP 인증 프로토콜 : 인증에 대한 정보를 주고 받을 때의 프로토콜, 기본적으로 제공하는 인증 프로토콜이 있음(2가지), 클라이언트 개발자와 서버 개발자가 협의에 따라 프로토콜 선정
    1. 기본 인증(basic auth)
    2. 다이제스트 인증(digest auth)
  • 기본 인증(basic auth)
    • 인증 정보를 Basic64로 인코딩하고, 서버에서는 Base64로 디코딩해서 사용함
    • 평문은 아니지만 Base64라는 이미 널리퍼진 매칭 방식의 인코딩을 하기때문에 금방 디코딩 할 수 있다는 단점이 있음
    • 인코딩된 문자열을 가지고 다른 서비스의 인증을 할 수 도 있음 : 같은 값을 가지고 인코딩을 하는 것이라서 기본 인증을 사용하는 다른 서비스에 해당 정보를 인증 요구 정보로 넘겨주면 될 수도 있음(사용자에 따라 같은 아이디 - 비밀번호를 쓰는 것이 다반사여서…)
  • 기본 인증 적용하기(일부 코드)
    • 인터셉터에서 기본 인증 요청 관련 헤더 조사
    • 헤더가 있을 경우에는 세션 설정
    • 헤더가 없을 경우에는 일단 인터셉터 통과 -> 커스텀 파라미터의 메소드에 따라 로그인 되어있지않을 때 예외 발생 -> 예외 처리(인증 요구 - 401 코드 응답, 인증 요구 정보 실어서)
/* BasicAuthInterceptor 중 */
public boolena preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String authorization = request.getHeader("Authorization");
    if (authorization == null && !authorization.startWith("Basic") {
        return true;
    }
    .
    .
    .
}

/* QuestionController 중 */
public String create(@LoginUser User user, QuestionDto questionDto) {
    
}

/* 커스텀 파라미터(핸들러) 메소드 중 */
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    User user = HttpSesstionUtils.getUserFormSerssion(webRequest);
    if (!user.isGuest()) {
        return user;
    }
    
    LoginUser loginUser = parameter.getParameterAnnotation(LoginUser.class);
    if (loginUser.required()) { //로그인 할 필요가 있는지 없는지 체크하는 메소드
        throw new UnAuthenticationException();
    }
}

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

해야할, 알아봐야할 것들

  • Rest API 의미와 Rest API 디자인 하기위해서는 어떤 원칙을 지켜야하나?
  • ResponseEntity, ResponseBody, Jackson 정리하기
  • HTTP 인증프로토콜(2) 다이제스트 인증에 대해 알아보기
  • 커스텀 파라미터(리졸버) 만드는 과정 되짚기
  • 스프링부트 스타터 디펜던시 알아보기
  • 레이어 구조 정리하기
  • JPA Persistence Context 한번 더 보기