다른 페이지에 접속할 때마다 계속해서 로그인을 반복을 방지하기 위해서 로그인 체크 기능 AOP를 적용하였습니다.

AOP

AOP란 관점 지향 프로그래밍으로 흩어진 공통점을 모아 만든 관점을 의미합니다.

@Aspect를 사용하면 AOP를 구현할 수 있기 때문에 반복되는 코드들을 정리할 수 있습니다.
또한 재사용 용이하며, 코드 누락 위험성이 적어집니다.

@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
@Log4j2
@SuppressWarnings("unchecked")
public class AuthCheckAspect {
    @Autowired
    private UserService userService;

    @Around("@annotation(com.idolticketing.idolticketing.aop.LoginCheck) && @ annotation(loginCheck)")
    public Object UserLoginCheck(ProceedingJoinPoint jp, LoginCheck loginCheck) throws Throwable {
        log.debug("AOP - User Login Check Started");
        Object[] signatureArgs = jp.getArgs();

        HttpSession session = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest().getSession();
        String sessionUserId = null;
        boolean isAdmin = false;
}

@Component

스프링으로 자동 관리를 해주기위해서 빈으로 등록하는 어노테이션입니다.

@Around

joinpointAdvice가 삽입되는데 Advice에는 5가지 시점(@After, @AfterReturning, @AfterThrowing, @Around, @Before )이 존재합니다. 그 중 Around에는 메소드 호출 자체를 가로채서 비즈니스 메소드 실행 전,후 모두에 처리할 로직을 삽입 할 수 있습니다.

저는 LoginCheck를 할 시 메소드 호출를 가로챌 수 있도록 만들었고, 세션 값과 isAdmin 값을 설정하였습니다.

@log4j2

logging이란 어떤 이벤트가 발생했을 때 로그가 남는 것을 의미합니다. 이 어노테이션을 사용하면 로그인기록이 남아 로그인이 되었는지를 확인할 수 있습니다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LoginCheck {
    public static enum Role{
        USER,ADMIN
    }
    Role type();
}

열거형인 enum을 사용하면 말 그대로 열거가 된다는 뜻입니다.

USER = 0, ADMIN = 1이 설정되었습니다.

@PostMapping("")
    @ResponseStatus(HttpStatus.CREATED)
    @LoginCheck(type = LoginCheck.Role.USER)
    public ResponseEntity<?> createBook(String userId, boolean isAdmin,
                                        @RequestBody BookDTO bookDTO,
                                        @ApiParam(value = "lang", defaultValue = "ko") @RequestParam String lang) {
        if (userId.equals(bookDTO.getUserId())) {
            bookService.createBook(bookDTO);
        } else {
            return new ResponseEntity<>("잘못된 접근입니다.", HttpStatus.BAD_REQUEST);
        }
        return new ResponseEntity<>(bookDTO, HttpStatus.OK);
    }

Book Controller에 와서 LoginCheck를 확인해 보면

@LoginCheck를 통해 @Around가 createBook 메소드가 호출되기 전 가로채 LoginCheck 메소드를 호출합니다.

타입은 Login.Role.USER로 설정되어 있어서 유저로 인식하게 됩니다.

 

이번에 로그인 체크 AOP를 다루면서 시점에 대해 자세히 공부할 수 있어서 좋았습니다.

joinpoint에 대해 매번 헷갈렸었는데 joinpoint에 Advice(시점이 있는)가 삽입되기 때문에 왜 joinpoint가 먼저와야 하는지에 대해 이해가 잘되어서 유용했습니다. 

Custom Exception을 사용하는 이유는 단순히 404 에러 등을 보다 자세한 상황에 대해서 알려주기 위해서 Custom Exception을 만듭니다.

implementation 'net.rakugakibox.util:yaml-resource-bundle:1.1'

시작하기 전 messageSource에서 사용할 리소스를 가져오기 위해서 gradle에 yaml resourece를 적었습니다.

//exception_en
unKnown:
  code: "-404"
  msg: "An unknown error has occurred."
userNotFound:
  code: "-403"
  msg: "This member not exist."
//excepion_ko
unKnown:
  code: "-404"
  msg: "알 수 없는 오류가 발생하였습니다."
userNotFound:
  code: "-403"
  msg: "존재하지 않는 회원입니다."

 

@RequiredArgsConstructor
@RestControllerAdvice
public class ExceptionAdvice {
    private final ResponseService responseService; 
    private final MessageSource messageSource;

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    protected CommonResult defaultException(HttpServletRequest request, Exception e) {
        
        return responseService.getFailResult(Integer.valueOf(getMessage("unKnown.code")), getMessage("unKnown.msg"));
    }

    @ExceptionHandler(CUserNotFoundException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    protected CommonResult userNotFoundException(HttpServletRequest request, CUserNotFoundException e) {
        
        return responseService.getFailResult(Integer.valueOf(getMessage("userNotFound.code")), getMessage("userNotFound.msg"))
    }

    private String getMessage(String code) {
        return getMessage(code, null);
    }
    private String getMessage(String code, Object[] args) {
        return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
    }
}
public class CUserNotFoundException extends RuntimeException {

    public CUserNotFoundException(String msg, Throwable t) {
        super(msg, t);
    }

    public CUserNotFoundException(String msg) {
        super(msg);
    }

    public CUserNotFoundException() {
        super();
    }
}

@RestControllerAdvice와 @ExceptionHandler를 설정하면 공통 코드로 분리하여 예외처리를 해버릴 수 있습니다.

만약 특정 Controller만 예외 처리를 하고 싶으시면 하위 Controller를 설정해서 예외처리가 적용될 것 입니다. 저 같은 경우에는 전체를 예외 처리하되, 만들 당시에 에러 로그를 읽어야 하는데 예외 처리로 넘어가져버려서 일단 주석 처리를 한 후 에러 부분을 수정한 후에 다시 예외 처리를 하는 식으로 해결을 하였는데 각자 편하신 대로 하셨으면 좋겠습니다.

 

@ExceptionHandler(Exception.class)는 예외 처리의 최상위를 담당하여 어떤 예외에 걸리지 않으면 이 예외를 통과

@ExceptionHandler(CUserNotFoundException.class)는 커스텀 예외 처리로 유저가 발견되지 않을 시 이 예외를 통과

 

messageSource는 code와 args와 LocaleContextHoler.getLocale()을 읽어 현재에 맞는 메세지를 가져옵니다.

CUserNotFoundException이라는 Custom Exception 만들어서 유저가 조회되지 않을 때 예외 상황을 알려줍니다.

  @PutMapping(value = "/login")
    public ResponseEntity<?> login(@RequestBody UserDTO userDTO, HttpSession session) {

        UserDTO userInfo = userService.login(userDTO);

        if(userInfo == null)
            throw new CUserNotFoundException( messageSource.getMessage("userNotFound.msg",null, LocaleContextHolder.getLocale()));
                                              LocaleContextHolder.getLocale()));
            ...생략

실제 UserController에서 Login을 하였을 때 유저의 정보가 null 값일 시 CUserNotFoundException이 실행되도록 하였습니다.

API 호출을 하였을 때 정상적으로 호출되는 것을 볼수 있습니다.

 

이번에 Custom 예외 처리를 하면서 이해가 안가는 부분들이 있었습니다. 공식 문서를 통해 @Controller와 @ControllerAdvice 클래스들은 Controller 메소드로부터 발생한 exception들을 처리하기 위해 @ExceptionHandler 메소드를 갖는다라고 하는데 Handler가 정확히 어떤 의미인지에 대해 궁금해져서 검색을 해보니 Controller에 @RequestMapping 어노테이션이 붙은 메서드를 의미로 사용한다라는 점을 알게 되어 흥미로웠습니다.

 

MVC패턴

MVC 패턴이란 Model–View–Controller의 앞글자만 따서 지칭한 것입니다. 

Model은 내부 비즈니스 로직을 의미하며 예로 DB를 의미합니다.

View는 말 그대로 사용자에게 보여지는 UI를 의미합니다.

Controller는 Vew와 Model 사이의 중개자 역할을 한다고 보면 됩니다. View에서 받은 요청을 처리하며, Model에게 어떻게 요청을 처리할지 알려주는 역할을 합니다. 

MVC 패턴을 사용하는 이유는 사용자에게 보여지는 UI 로직과 비즈니스 로직이 독립적이기 때문에 서로 상관하지 않고 원하는 부분만 따로 처리할 수 있으며, 유지 보수에 용이합니다.

MVC 패턴에 따라 Book,User,Help,Content Controller단을 만들었으며, Idol-Ticketing 서버 프로젝트를 하면서 제일 첫번째로 한 것은 계정 정보를 관리 할 수 있는 기능을 만드는 것이였습니다.

userController

@RestController
@RequestMapping(value = "/users")
public class UserController {

    @Autowired
    UserMapper userMapper;

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;

    }

    @PutMapping(value = "/login")//로그인
    public ResponseEntity<?> login(@RequestBody UserDTO userDTO, HttpSession session) {

        UserDTO userInfo = userService.login(userDTO);

        if (userInfo.isAdmin() == false) {
            SessionUtil.setLoginUserId(session, userInfo.getUserId());
            return new ResponseEntity<>(UserResponseDTO.builder()
                    .userId(userInfo.getUserId())
                    .name(userInfo.getName())
                    .code(201)
                    .message("일반 유저 로그인 성공").build(), HttpStatus.OK);
        } else if (userInfo.isAdmin()) {
            SessionUtil.setLoginAdminId(session, userInfo.getUserId());
            return new ResponseEntity<>(UserResponseDTO.builder()
                    .userId(userInfo.getUserId())
                    .name(userInfo.getName())
                    .code(202)
                    .message("관리자 로그인 성공").build(), HttpStatus.OK);
        } else {
            return new ResponseEntity<>(UserResponseDTO.builder()
                    .code(401)
                    .message("로그인 실패").build(), HttpStatus.NOT_FOUND);
        }
    }
REST API

 UserController를 만들때는 REST API URI 규칙을 지키며 회원가입, 로그인, 회원 수정, 로그아웃, 탈퇴 기능을 구성하였습니다.

REST는 Representational State Transfer의 약자로 클라이언트와 서버간에 데이터를 주고 받을 때 방식에 대한 아키텍처 스타일입니다. 

 REST API를 사용하는 이유는 웹의 장점(네트워크 연결만 되면 어디서든 실행이 가능하다.)을 활용할 수 있는 아키텍처로 만든 것이 REST이며, 가장 큰 특징은 각 요청이 어떤 동작이나 정보를 위한 것인지를 그 요청의 모습 자체로 추론이 가능하기 때문입니다.

REST API URI 규칙
  • 소문자를 사용한다.
  • 언더바 대신 하이픈을 사용한다.
  • URI의 마지막에는 슬래시를 포함하지 않는다.
  • 계층관계를 나타낼 때는 슬래시 구분자를 사용해야 한다.
  • 파일 확장자는 URI에 포함시키지 않는다.
  • 전달하고자 하는 자원의 명사를 사용하되, 컨트롤 자원을 의미하는 경우 예외적으로 동사를 허용한다.
  • URI에 작성되는 영어를 복수형으로 작성한다.

userDTO

@Data
@Builder
public class UserDTO {
    private String userId;                      //아이디
    private String name;                        //이름
    private String password;                    //비밀번호

    public UserDTO(){

    }

    public UserDTO(String userId,String name,String password){
        this.userId = userId;
        this.name = name;
        this.password = password;
    }
}

실제 DB에 들어갈 값들을 작성하여 userDTO로 구성하였습니다.

DTO를 사용하면 클라이언트가 요청한 데이터를 담아 전달하는 역할을 합니다.

DB Connection은 Java와 DB를 연결 객체로 연결될 때마다 객체를 만들면 db의 정보를 넘겨야하기 때문에 소요시간이 오래걸립니다. 그렇기 때문에 DB Connection Pool 값을 늘려 미리 DB Connection 객체를 많이 만들어 놓습니다.

userService

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    @Override
    public UserDTO login(UserDTO userDTO) {
        return userMapper.findByIdAndPassword(userDTO);
    }

}

@Service 을 통해 스프링 빈(Spring bean)이 자동으로 생성됩니다.

이는 스프링 특징인 제어의 역전(IoC)인데 객체의 생성 및 제어권을 사용자가 아닌 스프링이 맡는 것입니다.

 

userMappper.xml

    <select id="findByIdAndPassword" resultType="dto.UserDTO">
        SELECT userId,name,password,email,phone,address,isAdmin
        FROM user
        WHERE password = #{password}
        AND userId = #{userId}
    </select>

userMapper.java

@Mapper
public interface UserMapper {
     UserDTO findByIdAndPassword (UserDTO userDTO);//로그인
}

Mybatis를 사용하여 mapper 코드를 xml으로 작성 후 userMapper 인터페이스를 만들어 @Mapper를 설정해 주었습니다. 이렇게 하면 mapper 코드들을 한번에 관리할 수 있다는 장점이 있습니다.

 

postman API 호출

post, 회원가입 ,                          put,  로그인,                                 patch, 회원 수정

             put,로그아웃,                                                             delete, 회원탈퇴

 

API 호출까지 성공적으로 되는 것으로 파악되었으며, 이후 User Controller 뿐만 아니라, help,content,book Controller도 해결하였습니다.

Controller를 다루는 것이 처음은 아니였지만 REST API로 작업하는 것은 처음이였기 때문에 주소창에 값을 넘긴다는 것은 알지만 어떻게 넘겨야하는 것인지에 대해 명확히 개념이 잘 안서던 REST API에 대해 Patch는 리소스의 일부분의 값만 수정할 때 사용하고, Put은 리소스의 전체 값을 수정할 때 사용하며 get은 @RequestBody을 쓰면 안되고 @RequestParam을 써야 한다고 정리할 수 있는 좋은 경험이 되었습니다.

이번에 지속적 통합 (Continuous integration), 지속적 배포(Continuous Delivery)에 대해서 적용해 보았습니다.

통합하는 이유는 각각의 목적으로 관리하기 위해 독립적으로 여러 브랜친 나누어진 소스코드들을 하나의 브랜치로 통합하고, 배포하기 위해서입니다. 

 

통합 배포 도구는 TeamCity,AWS CodeDeploy,jenkins,DeployBot 등등 찾아보니 정말 많은 종류들이 있었지만 그 중에서도 이번에 git에 있는 코드들을 통합하기 위해 사용해 봤던 툴은 젠킨스였습니다. 

젠킨스를 사용한 가장 큰 중요한 점은 유료 툴 중에서도 무료 툴이였으며, 현재까지 사용자가 많다는 점, 플러그인 지원이 좋은 편입니다. 또한 설치 및 사용이 간단합니다. 

 

jenkins CI

젠킨스를 사용하기 위해서는 우선적으로 git에 있는 프로젝트부터 가져와야 합니다.

첫번째로 원하는 git 프로젝트의 주소창을 그대로 가져와 줍니다. 

위에서 프로젝트의 주소를 가져왔다면 소스코드 관리에서는 레파지토리 주소를 가져와 주고 Credientials에서는 git에서 따로 token(엑세스 권한)을 만든 다음 Add로 설정합니다.

 

빌드 유발에서는 GitHub hook trigger for GITScm polling을 선택을 합니다.

webhook이란 클릭 시 실행되는 이벤트를 통해 git에 트리거를 유발시켜 빌드를 받을 수 있게 하는 것입니다.

트리거라는 단어를 정리하지 않고 넘어갔던 부분이 문제가 되었습니다. 트리거를 직역하면 방아쇠라는 뜻을 의미합니다.

이것을 컴퓨터 용어로 좀 더 쉽게 이해하면 트리거라는 것은 자동으로 실행하게 할 수 있는 역할을 한다는 것입니다.

각자에게 맞는 gradle 버전을 설정해주신 다음 Tasks에 bootjar를 작성해줍니다. 

 

빌드 후 조치에는 log text에 BUILD SUCCESFUL을 적은 다음 Script에 SANPSHOT.jar 파일이 있는 경로를 작성해 줍니다.

 

java gradle 기준 경로 위치

java -jar build/libs/본인 파일명-0.0.1-SNAPSHOT.jar &

 

Spring과 tomcat이 실행되는 것을 알 수 있으며, port 8080으로 API 실행을 할 시 제대로 전송되는 것을 볼 수 있습니다.

 

CD 적용하기

webhook에서도 git으로 push를 해도 연결이 안되는 문제가 있었기 때문에 결국 로컬로 접속해 연결이 되는지 확인하기로 했습니다.

We couldn’t deliver this payload: failed to connect to host

https://ngrok.com/

 

ngrok - Online in One Line

Zero Trust Add SSO, Mutual TLS, IP Policy, and webhook signature verification.

ngrok.com

로컬에서 webhook에 접속하기 위해서는 ngrok 프로그램을 다운받아 주소를 받아야합니다.

ngrok http 젠킨스포트

Forwarding에 있는 주소를 복붙하셔서 webhook URL에 적으면 됩니다.

다시 git에 가서 push를 해보았을 때 정상적으로 push가 된 것을 볼 수 있습니다.

 

이번 젠킨스 CI/CD를 하면서 CI에 비해 CD에 대한 이해가 부족했던 것에 깨달았습니다. 지속적 배포라는 것이 자동화라는 것을 단순히 책으로만 외우다가 직접 git으로 push를 했을 때 젠킨스에서 빌드가 된다는 것을 보았을 때 이렇게 자동화가 이루어 지는 것이라면 너무 많은 빌드가 이루어지는 것보단 적은 빌드로 완성도 높은 프로그램을 만드는 것이 좋을 것이라 생각되었습니다.

아이돌 티켓팅 서버에서 가장 중요하게 생각하는 부분은 예매 API이였습니다. 

실제로 짧은 시간에 가장 많은 이벤트가 발생하기 때문입니다.

따라서 로그인 API와 예매 API를 성능 테스트하였습니다.

시나리오 : 고척돔 기준 6만명이 접속해 1분안에 매진된다는 상황, 초당 1000tps를 목표로 성능테스트 진행
목표 : Min과 Max의 격차를 최소화 할 것, RPS : 1000.
테스트 1

가장 먼저 테스트했던 시나리오 결과입니다. min은 0.001초, max는 10.528로 격차가 크며, RPS는 191으로 적습니다.

 

- 원인 및 해결방안

B-tree 구조에서 삽입을 할 시 데이터를 조회 후 노드를 추가하고 자식 노드가 부모 노드보다 더 많다면 상향식으로 적절한 위치를 찾아가기 때문에 시간복잡도가 증가합니다. 더불어 현재 만들어진 Mysql에서 PK가 2개였기 때문에 index 값을 삭제할 시 RPS 값이 증가한다는 것을 확인하고 primary key 중에서 userId를 삭제를 했습니다.

*여기서 헷갈리는 부분이 PK가 두개라는 부분이 무결성의 원칙을 어기는 것이냐는 것이였습니다. 하지만 결론적으로 두 칼럼을 합쳤을 때 중복이 아니면 무결성의 원칙을 지키는 것이라는 것을 알게 되었습니다.

Ex) [A=1,B=1],[A=1,B=2]는 중복된 것이 아니다.

테스트 2

RPS가 거의 3배 값인 351로 증가한 것을 볼 수 있습니다. 하지만 min 값은 max 값은 오히려 11.611초로 증가했습니다. 

 

마지막으로 해결해야하는 부분은 시간적 요소였습니다. 자바 프로그램에 DBMS로 커넥션을 생성하려면(생성할 때마다 db정보(db host, port, connection-poolsize, id, password, db name, encoinding)를 만듦) 시간이 많이 소요됩니다. 그러므로 미리 db 커넥션을 많이 만들어놓으므로 그 시간을 단축할 수 있도록 만드는 것입니다.

application.properties에서 connection-pool 값을 10에서 20으로 변경하였습니다.

spring.datasource.maximum-pool-size=20
테스트 3

최종적으로 min 값은 0.001초 max 값은 8.774초 RPS는 357로 증가한 것을 볼 수 있습니다.

마무리로 RPS를 증가시키기 위해서 scale-out을 이용하여 다른 서버를 3개 정도 증설하면 기존 원했던 RPS 1000에 도달 할 수 있습니다.

 

locust을 이용해서 실제처럼 RPS 값을 볼 수 있다는 점이 재밌었던 기억이 납니다. 하지만 제 컴퓨터의 사양에 따라 달라졌던 값이였기 때문에 이것을 토대로 이렇구나를 참조하는 것이지 백프로 믿고 나아가기에는 문제가 있습니다.

또한 locust말고도 많은 성능 테스트가 있기 때문에 좀 더 다양한 경험을 쌓는 게 중요하지 않을 까 싶습니다.

Junit을 사용하다보니 현재 버전(Junit5)과 Junit4가 많아서 헷갈리는 점이 많았습니다. 오늘 포스팅에서는 직접 테스트해보면서 사용했던 코드와 차이 났던 부분에 대해서 정리해보았습니다. 또한 저는 Controller 부분을 테스트해보는 것이 중요하다고 생각했는데 사용자에게 요청을 받고, Model 객체를 통해 응답 하기 때문입니다.

@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = BookController.class)
class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private BookService bookService;

    @MockBean
    private BookMapper bookMapper;

    @Autowired
    private ObjectMapper objectMapper;
    private Object val$mvcResult;

    @Test
    void createBook() throws Exception {
        BookDTO bookDTO = BookDTO.builder()
                .userId("test3")
                .id(20)
                .build();
        String bookDTOString = objectMapper.writeValueAsString(bookDTO);

        MvcResult result = mockMvc.perform(post("/book")
                        .param("userId", "test3")
                        .param("id", "20")
                        .content(bookDTOString)
                        .contentType(MediaType.APPLICATION_JSON))
                .andReturn();

        String content = result.getResponse().getContentAsString();

        ObjectMapper objectMapper = new ObjectMapper();
        BookDTO response = objectMapper.readValue(content, BookDTO.class);

        assertEquals("같습니다.", bookDTO, response);
    }
}

Junit 테스트에서는 실제 객체값을 사용할 수 없기 때문에 가짜 값인 Mock 객체를 사용합니다.

Mock 객체 사용시 장점은 아래와 같습니다.

  • 단언 실패시 실패한 테스트의 이름이 표기되어 빠르게 문제 파악 가능
  • 현재 실패한 테스트가 다른 테스트에 영향을 끼치지 않아 실패 테스트 디버깅에 필요한 시간을 단축.
  • 모든 케이스가 실행되었음을 보장. 단언실패시 이후의 테스트는 실행되지 않으므로 분리시켜야지 모든 케이스가 실행됨을 보장할수있다

BookDTO 값과 mock 객체를 만들어서 assetEquals로 서로 같은 값인지에 대해 비교하는 테스트를 진행하였습니다.

Runwith vs ExtendWith

Junit4까지는 Runwith(SpringRunner.class)을 사용했지만 junit5으로 넘어오면서 ExtendWith을 사용하게 되었습니다.

 

RunWith(SpringRunner.class)에 대해 정리해보자면

테스트 시 @RunWith(SpringRunner.class) -> SpringRunner.class -> SpringBootApplication이 지정된 클래스를 실행하게 되면서 결론적으로 스프링 컨테이너(.class)를 사용합니다.

(스프링 컨테이너란 빈을 관리해주는 공간이다.)

하지만 ExtendWith으로 변경 되면서 @ExtendWith 어노테이션은 @SpringBootTest를 포함으로 지정한 컨트롤러를 실행시켜 주는 역할을 하기 더 이상 @RunWith 어노테이션을 사용할 이유가 사라졌습니다.

 

assertThat

 

그 다음으로 Junit5로 넘어오면서 assert 메소드에도 변경사항이 생겼습니다 

 

Junit5에서 사용하는 assert 종류를 보면

  • assertArrayEquals  : 두 배열을 비교하여 일치 여부를 판단한다.
  • assertEquals  : 두 값을 비교하여 일치 여부를 판단한다
  • assertNull assertNotNull : 객체의 null 여부를 확인한다.
  • assertNotSame,assertSame : 두 변수가 동일한 객체를 참조하는지 확인한다.
  • assertTrue,assertFalse : 특정 조건이 true false인지 판단한다.
  • fail : 테스트를 실패하게 만들었을 때 사용한다.

여기서 Junit4에서 Junit5으로 넘어오게 되면서 빠지게 된 코드여도 assertThat을 사용하면 코드가 좀 더 깔끔해 보이는 것 같아보였습니다.

예를 들어 차례대로

assertEquals,assertTrue,assertFalse을 assertThat으로 사용 했을 때입니다.

1. assertEquals(“expected”, “actual”);
2. assertTrue(value);
3. assertFalse(value);

 

1. assertThat(actual, is(expected));
2. assertThat(actual, is(true));
3. assertThat(actual, is(false));

 

assertThat 정리되면서 실제 값과 기대 값의 차이를 확실히 구분할 수 있었기에 Junit5에서도 꼭 사용하고 싶어 어떻게 사용하나 봤더니 hamcrest 라이브러리를 추가하면 해결되는 것이였습니다.

import static org.hamcrest.MatcherAssert.assertThat;

assertEquals 등을 사용해보면서 assertThat보다 더 간결하게 코드를 작성할 수 있다고 느꼈습니다.

그외 @Before -> @BeforeEach, @After -> @Aftereach 등 더 바뀐 게 많습니다. 

 

하지만 Junit 테스트라는 것은 한계가 있습니다. 실제 DB를 타지 않고 본인들이 직접 값을 만들어 가짜로 비교를 하는 것이기 때문에 이 테스트의 정확도를 의심해 볼 필요가 있습니다. 그럼에도 불구하고 이 테스트를 진행하는 이유는 우리가 아예 테스트를 진행하지 않고 시장에 불확실한 프로그램을 내놓는것과 적어도 어느정도까지의 확실함을 가진 프로그램을 내놓는 것은 전혀 다를 것 입니다. 이번 Junit 테스트를 하면서 저도 정확도에 대해서 의구심을 떨칠 수 없었던 것은 아니지만 그렇기 때문에 더 정보를 찾아봤던 것 같습니다.

'PROJECT > 티켓팅 서버 프로젝트' 카테고리의 다른 글

Login Check 기능 AOP 적용  (0) 2022.11.03
Custom Exception 적용하기  (0) 2022.11.01
MVC 패턴 Controller 적용하기  (0) 2022.10.05
Jenkins CI/CD 적용하기  (0) 2022.09.20
locust 성능 테스트 시나리오  (0) 2022.08.29

+ Recent posts