Spring

- Test 과정 이모저모

Optional의 null + orElseThrow

서비스단 테스트 과정에서 Repository는 Mock으로 선언하고

ToDoCard toDoCard = toDoCardRepository.findById(id).orElseThrow(() -> new NullPointerException("해당 할일카드를 찾을 수 없습니다."));

이 코드에서 찾는 할일카드가 없는 오류를 냈어야 했다. 

 

처음에는 null 받아올 테니까 

when(toDoCardRepository.findById(id)).thenReturn(null);
Throwable exception = assertThrows(NullPointerException.class, () -> {
    toDoCardService.getToDoCard(id);
});

when(toDoCardRepository.findById(id)).thenReturn(null);

이렇게 짰는데 이러면 아예 Null값 자체를 확정지어버리게 되어 오류가 발생했다.

바로 null값이 orElseThrow()를 호출할 수 없기 때문이었다.

 

when(toDoCardRepository.findById(id)).thenReturn(Optional.empty());
Throwable exception = assertThrows(NullPointerException.class, () -> {
    toDoCardService.getToDoCard(id);
});

null을 Optioanl.empty()로 고치니까 해결되었다.

앞으로 Optional에 딸린 null값은 Optional.empty로 하면 좋을 듯!

 

 

HttpStatusCode.valueof

클라이언트에게 Response를 보낼 때 ResponseEntity에 ResponseDto, header, 상태코드를 넣어주는 방식으로 반환하는 구조로 코드를 짰다.

테스트 할 때에 Dto(payload) 부분에는 상태코드가 없기 때문에 ResponseEntity에서 직접 상태코드를 받아야 한다.

그런데 ResponseEntity.getStatusCode()는 반환형이 HttpStatusCode라는 인터페이스다.

그래서 테스트 시

assertEquals(response.getStatusCode(), 200);

라고 안일하게 적었다가는 오류가 나고 만다.  

(Expected: "200 OK", Actual: "200" 이렇게 난다. 타입 자체가 다르기도 하고..)

 

assertEquals(response.getStatusCode(), HttpStatusCode.valueOf(200));
assertEquals(response.getStatusCode().value(), 200);

둘 중에 맛난 것으로 고쳐쓰자.

 

 

 

Spring Security + Controller Test

스프링 시큐리티가 적용된 채로 컨트롤러 테스트를 하려면 난감한 점이 많다.

컨트롤러 단에서는 필터 단에서의 토큰 검증이 필요가 없는데(이것 땜시 테스트 오류 발생) 필터 단에서 보내주는 UserDetails에 대한 검증은 필요하기 때문이다.(@Authentication 이 인자 부분에 달려있음!)

 

그래서 우리는 1차로

@WebMvcTest(
        controllers = {UserController.class, ToDoCardController.class, CommentController.class},
        excludeFilters = {
                @ComponentScan.Filter(
                        type = FilterType.ASSIGNABLE_TYPE,
                        classes = WebSecurityConfig.class
                )
        }
)

excludeFilter를 통해 기존 jwt 검증 필터들을 무시해주고.(websecurityconfig에서 ignore 쓰는 방법도 있다.)

 

private MockMvc mvc;

@BeforeEach
    public void setup() {
        mvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(springSecurity(new MockSpringSecurityFilter()))
                .build();
    }

 

2차로 가짜 mvc 객체에 별 기능없는 커스텀필터를 적용시켜준다.

근데 진짜 별 기능없으면 안되고 UserDetails를 받아올 수 있는 최소한의 조건은 있어야 한다.

SecurityContextHolder.getContext().setAuthentication(
        (Authentication) ((HttpServletRequest) req).getUserPrincipal()
);

 

 

마지막으로

private void mockUserSetup() {
    String username = "dongha";
    String password = "12345678";
    User testUser = new User(username, password);
    UserDetailsImpl testUserDetails = new UserDetailsImpl(testUser);
    mockPrincipal = new UsernamePasswordAuthenticationToken(testUserDetails, "", testUserDetails.getAuthorities());
}

테스트를 위한 Principal까지 가져와주면 클라이언트에서 보내듯이 테스트를 진행할 수 있다..

 

 

@Test
@DisplayName("ToDoCardController, 할일카드 작성 기능")
void test2() throws Exception {
    //given
    this.mockUserSetup();
    String title = "테스트용 할일카드 제목";
    String contents = "테스트용 할일카드 내용";

    ToDoCardRequestDto requestDto = new ToDoCardRequestDto(
            title,
            contents
    );

    String toDoCardInfo = objectMapper.writeValueAsString(requestDto);

    //when - then
    mvc.perform(post("/api/todocards")
                    .content(toDoCardInfo)
                    .contentType(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .principal(mockPrincipal)
            )
            .andExpect(status().isOk())
            .andDo(print());
}

 

알고리즘

- 프로그래머스 : 조이스틱(Level 2, 구현)

느낀 점

.

'TIL' 카테고리의 다른 글

[23.12.06]  (0) 2023.12.06
[23.12.05]  (1) 2023.12.06
[23.11.30]  (1) 2023.12.01
[23.11.29]  (0) 2023.11.29
[23.11.28]  (0) 2023.11.28

Spring

- Mockito 기초

Mockito는 Mock이라는 가짜 객체를 이용해 테스트를 용이하게 해주는 프레임워크이다.

Mock 객체는 실제 사용하는 모듈을 사용하지 않고 실제의 모듈을 "흉내"내는 "가짜" 모듈을 작성하여 테스트의 효용성을 높이는 데 사용하는 객체이며 Mockito의 핵심이다.

 

Mock 생성에는 크게 4가지 annotation이 사용된다. 바로 @Mock, @MockBean, @Spy, @InjectMocks

 

먼저 @Mock과 @MockBean은 가짜 객체 생성을 하는 어노테이션으로 쓰임새는 비슷하지만 약간의 차이점이 있다.

Spring Boot Container가 테스트 시에 필요하고, Bean이 Container에 존재한다면 @MockBean을 사용, 아니라면 @Mock 을 사용하라고 한다.

예를 들면 Service 테스트에 Repository의 주입은 필수적인데 우리는 이 repository를 사용하지는 않는다.(DB에 잘 저장되었는 지는 Repository 테스트를 따로 하기 때문에)

따라서 주입할 repository는 명목상 필요하고 repo의 메소드는 필요없기 때문에 @Mock을 선언해준다.

그러나 Controller 테스트에서 Service의 주입은 필수적임과 동시에 Service 내의 메소드를 호출해야 그 반환값이 Response로 쓰이는 경우가 많다. 이 때는 Service를 Bean으로 등록해야하기 때문에 @MockBean 을 사용한다고 나는 이해했다.

공통으로 이 가짜 Mock 객체가 어떤 기능을 하게 하고 싶다면 stubbing이라고 mock 객체 내의 메소드를 지정해주면 된다.

 

@Spy는 실제 사용되는 객체이다. (가짜 mock 사이의 진짜 spy라는 뜻인가?)

이 어노테이션을 쓰면 stubbing을 따로 하지 않으면 기존 객체의 실제 로직을 수행한다.

 

@InjectMocks 생성된 Mock 객체들을 자동으로 주입해주는 어노테이션이다.

public class CommentSerivce() {
	UserService userService;
    ToDoCardService toDoCardService;
}

이렇게 생긴 CommentService가 있다고 했을 때

@Mock
UserService userService;

@Mock
ToDoCardService toDoCardService;

@InjectMocks
CommentService commentService;

 

이렇게 해주면 @Mock으로 생성된 userService와 toDoCardService가 자동 주입된다.

알고리즘

- 프로그래머스 : 과제 진행하기(Level 2, 스택)

느낀 점

.

'TIL' 카테고리의 다른 글

[23.12.05]  (1) 2023.12.06
[23.12.01]  (0) 2023.12.04
[23.11.29]  (0) 2023.11.29
[23.11.28]  (0) 2023.11.28
[23.11.22]  (1) 2023.11.22

Spring

 - AOP

AOP(Aspect Oriented Programming)은 부가기능과 핵심기능의 관점(관심)이 다르다는 점에 집중해서 핵심기능과 부가기능을 분리하여 설계, 개발하는 것이다. 부가기능을 모듈화하여 핵심기능에 붙여준다고 생각하면 된다.

 

UserService                    PostService

before()                           before()

getUser()                        getPost()

after()                              after()

이런 식으로 각각 서비스의 조회 메소드가 실행된다 할 때 핵심기능인 get~~()는 제쳐두고

핵심기능 전후에 호출되는 before after 메소드는 공통으로 관리하자는 것이다.

가로 영역을 잘라내어 생각한다고 해서 AOP를 크로스 커팅 이라고 부르기도 한다.

 

AOP는 공통으로 쓰이는 반복되는 부가기능들을 하나로 관리하여 재사용하자는 것이며

OOP는 비즈니스 로직의 모듈화, AOP는 부가기능(로그, 오류 처리, 캐싱 등등)의 모듈화이다.

 

스프링에서는 런타임 시점에 AOP를 적용한다. 이 방식은 클래스 로더에 올라간 빈에만 적용할 수 있다는 단점이 있지만 복잡한 옵션이나 특별한 컴파일러 없이도 가능하다는 장점이 있다고 한다.

 

각종 용어들

Advice: 부가기능 로직을 정의하는 곳.(부가기능 구현 + Pointcut의 전, 후, 전후 모두 적용할 지..) 

Pointcut: Advice가 적용될 위치(어떤 컨트롤러의 어떤 메소드에 적용할 건지..)

Aspect: Advice와 pointcut들을 모듈화 한 단위. @Aspect가 Bean 클래스에만 적용이 가능하므로 @Component도 해줘야함.

 

AOP 동작방식

AOP 적용 전
AOP 적용 후

 

Spring이 프록시 객체를 중간에 삽입해 준다. 호출되는 메소드의 input, output은 변동이 없다.

알고리즘

- 프로그래머스 : 방문 길이(Level 2, 구현)

느낀 점

.

'TIL' 카테고리의 다른 글

[23.12.01]  (0) 2023.12.04
[23.11.30]  (1) 2023.12.01
[23.11.28]  (0) 2023.11.28
[23.11.22]  (1) 2023.11.22
[23.11.21]  (0) 2023.11.21

Spring

 - Spring security Role 관련 이모저모

저번 주 내내 고생한 프로젝트에서 인증 파트를 맡았는데 스프링도 잘 모르는 상태로 security 를 다루려고 하니 어려움이 많았는데 가장 시간을 많이 할애했던 Role에 대해 이야기 해보려 한다.

 

Role의 쓰임새는 사용자의 Role에 따라 특정 url에 대한 접근을 제어하기 위함이다.

가장 많이 쓰이는 메소드는 requestMatchers.~~~이다.

//Role에 관계없이 모든 사용자가 이용가능
.requestMatchers("/api/user/**").permitAll()

//Role이 USER인 사용자만 이용가능. 근데 여기서는 "ROLE"을 앞에 알아서 붙여주기 때문에 "ROLE_USER"하면 오류
.requestMatchers({uri}).hasRole("USER")

//hasRole과 동일한 기능
.requestMatchers({uri)).hasAuthority("ROLE_USER")

 

hasAnyRole 로 해서 여러 권한도 넣어줄 수 있고 한데 주의할 건 Role이 들어간 메소드는 ROLE_을 앞에 붙여준다는 의미이다. 오류 메시지에도 잘 나오니까 크게 걱정할 필요는 없다.

 

그럼 과연 이 ROLE들을 어떻게 구분하는 것일까. 비밀은 바로 UserDetails에 있다.

 

UserDetails는 security에서 제공하는 인터페이스로 사용자에 대한 인증에 이용된다.

public interface UserDetails extends Serializable {

    /**
     * Returns the authorities granted to the user. Cannot return <code>null</code>.
     * @return the authorities, sorted by natural key (never <code>null</code>)
     */
    Collection<? extends GrantedAuthority> getAuthorities();

    /**
     * Returns the password used to authenticate the user.
     * @return the password
     */
    String getPassword();

    /**
     * Returns the username used to authenticate the user. Cannot return
     * <code>null</code>.
     * @return the username (never <code>null</code>)
     */
    String getUsername();

    /**
     * Indicates whether the user's account has expired. An expired account cannot be
     * authenticated.
     * @return <code>true</code> if the user's account is valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    boolean isAccountNonExpired();

    /**
     * Indicates whether the user is locked or unlocked. A locked user cannot be
     * authenticated.
     * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
     */
    boolean isAccountNonLocked();

    /**
     * Indicates whether the user's credentials (password) has expired. Expired
     * credentials prevent authentication.
     * @return <code>true</code> if the user's credentials are valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    boolean isCredentialsNonExpired();

    /**
     * Indicates whether the user is enabled or disabled. A disabled user cannot be
     * authenticated.
     * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
     */
    boolean isEnabled();

}

 

함수 명들만 보더라도 사용자 인증에 가져다 쓰라고 만든 것 같다.

UserDetails에서 Role과 관련된 메소드는 getAuthorities()이다.  본인이 커스텀하기 나름이겠지만 이 함수 리턴값에 특정 유저에 대한 모든 권한 리스트를 담아주면  된다.

 

이번에 가장 헷갈렸던 부분!

hasRole("USER")가 호출 -> securitycontextholder에서 UserDetails 가져옴 -> UserDetails.getAuthorities() 에 "ROLE_USER" 가 존재하는지 확인 -> 요청을 막거나 허가.

 

컨트롤러 단에서 권한을 검증하는 @Secured @PreAuthorize도 있다.

간혹 jwt claim에 권한을 넣어주는 경우도 있는데 이는 클라이언트에게 유저의 권한을 활용할 수 있도록 하거나

jwt를 완전히 신용한다면 따로 UserDetails를 검사하지 않고 넘겨주는 필터를 적용할 수도 있겠다.

알고리즘

- 프로그래머스 : 압축(Level 2, 해시, 문자열)

느낀 점

.

 

'TIL' 카테고리의 다른 글

[23.11.30]  (1) 2023.12.01
[23.11.29]  (0) 2023.11.29
[23.11.22]  (1) 2023.11.22
[23.11.21]  (0) 2023.11.21
[23.11.20]  (0) 2023.11.20

 Spring

 - Getter, Setter의 적절한 사용

Controller를 드나드는 다양한 DTO들.. 어떤 DTO는 getter만 있기도 하고, setter만 있기도 하고, 둘 다 있기도 하고 생성자가 있기도 하다. 오늘 코드 리뷰 중 팀원 분께서 request dto는 setter가 필수라는 말씀을 해주셔서 이 이유에 대해 알아보려 한다.

 

request dto를 받을 때는 클라이언트에서 json 데이터가 넘어온다 가정을 하고 @RestController를 많이 사용한다.

이때 SAbstractJackson2HttpMessageConverter 에서 Json -> dto 객체로 변환이 일어나는데. 여기서 json의 key-value 값을 객체의 field-value 값으로 전환시켜주기 위해서는. 필드값을 세팅하기 위해 setter가 필요하다.

 

response dto를 내보낼 때에는 dto 객체를 json body로 바꾸어줘야 한다.

이제는 반대로 dto의 field-value 값을 key-value 값에 넣어주어야 하니, field-value 값을 빼낼 getter가 필요한 것이다.

 

dto <-> json body 의 변환을 위해 getter, setter 가 필요한 이유는 납득이 된다.

그렇다면 모든 dto, entity 등등에 getter, setter, allarg, noarg 다 하면 손해볼 게 없는 것 아닌가?

둘 다 남발하면 안된다!!

 

getter를 남발하면 안되는 이유.

필드를 private로 잘 설정해 놓고 모든 필드값을 getter로 꺼내올 수 있다면 캡슐화의 의미가 퇴색된다.

 

setter를 남발하면 안되는 이유.

1. setter로 인해 모든 곳에서 객체 값의 변경이 가능할 텐데, 그러면 객체 값을 변경해주는 메소드의 의미가 퇴색된다. + 의도치 않게 값이 변경될 가능성이 있기 때문에 지양해야 한다.

2. setter를 꼭 필요한 필드에만 만들어 놓으면 값을 변경하는 케이스가 희귀하다는 의미이므로 의도 파악이 명확해진다.

알고리즘

- 프로그래머스 : 방금그곡(Level 2, 문자열)

느낀 점

.

'TIL' 카테고리의 다른 글

[23.11.29]  (0) 2023.11.29
[23.11.28]  (0) 2023.11.28
[23.11.21]  (0) 2023.11.21
[23.11.20]  (0) 2023.11.20
[23.11.17]  (0) 2023.11.17

 Spring

- 뉴스 피드 프로젝트 1일차

취준생을 위한 뉴스 피드 프로젝트를 설계했다.

주요기능은 회원가입, 로그인 + 인증, 프로필 설정/수정, 게시글 CRUD 이다.

 

ERD

 

Wire Frame

 

 

Pull Request에 이어서 처음으로 코드 리뷰도 해봤다.

위 예시와 다르게 팀원분에게 예의를 지키는 리뷰를 하도록 하자.

리뷰를 마치고 고를 수 있는 옵션이 세가지가 있다.

comment(그냥 댓글), approval(좋네요 승인), request changes(다시 고치고 승인받으러 오세요)

request changes가 하나라도 있으면 pull request가 불가하다.

 

- @RestControllerAdvice

@ControllerAdvice 와 @ResponseBody를 합친 annotation이다.

전역의 컨트롤러나 설정한 컨트롤러 내의 exception을 모두 처리할 수 있다.

 

@RestControllerAdvice(basePackageClasses = UserController.class)
public Advisor {

    @ExceptionHandler(NullPointerException.class)
    public void exceptionHandler(Exception e){
        System.out.println(e.getMessage());
    }
    
}

 

이렇게 설정해주면 UserController에서 nullpointerException이 발생했을 때 exceptionHandler 메소드를 실행시켜준다.

 

알고리즘

- 프로그래머스 : 택배 배달과 수거하기(Level 2, 그리디)

느낀 점

새로운 협업에 들어갔는데 초반 설계부터 다들 친절하고 존중이 갖춰진 상태라 출발이 매우 좋다. 민폐끼치지 않게 열심히

'TIL' 카테고리의 다른 글

[23.11.28]  (0) 2023.11.28
[23.11.22]  (1) 2023.11.22
[23.11.20]  (0) 2023.11.20
[23.11.17]  (0) 2023.11.17
[23.11.16]  (0) 2023.11.16

 Spring

- x-www-form-urlencoded   vs  application/json

이 둘은 HTTP request 의 header에 실려 있는 content-type의 한 종류다.

HTTP request는 Reqeust Line - HTTP header - Empty Line - Message Body 이렇게 네 파트로 나누어져 있다.

이때 Message Body에 어떤 타입(텍스트, 파일 등등)이 들어갈 것인지를 header에 명시해 줄 수 있는데 Content-Type 필드에 명시해주면 된다.

 

일단 x-www-form-urlencoded는 전통적인 Content-Type으로 html의 form 태그를 통해 보내주면 이 방식이기 때문에 인기가 많았었다. key1=value1&key2=value2 이런 식으로 넘어온다.

Controller 단에서 api 매핑할 때, annotation을 생략한 경우와 @ModelAttribute를 사용한 경우에 이러한 방식으로 request data를 받는다.

 

application/json은 현재 가장 많이 쓰이는 Content-Type으로 json 형태로 데이터가 넘어온다.

 

- @JsonInclude

Json 처리에 유용한 jackson 에서 제공하는 annotation으로 json에서 원하지 않는 값들을 제외할 수 있다.

@JsonInclude(JsonInclude.Include.NON_NULL)

 

@RequestMapping("JsonInclude.Include.") //넝담~

ALWAYS - 모든 데이터 포함

NON_NULL  - null인 데이터를 제외

NON_ABSENT - null이거나 absent인 데이터 제외

NON_EMPTY - null, absent, 빈 자료구조, "" 를 제외

NON_DEFAULT - NON_EMPTY인 경우 + 기본 형이 default(int면 0 boolean이면 true)인 경우 제외

 

알고리즘

- 프로그래머스 : 배달(Level 2, 다익스트라)

느낀 점

과제 기능 구현은 다 해서 다행이지만 설계 시에 Entity 연관 관계에 대한 고민과 공부를 더 해야한다고 느꼈다. 또한 튜터님마다 프로젝트나 코드짜는 방식이 다 다르신데 이것의 장단점에 대해서도 궁금해진 하루.

'TIL' 카테고리의 다른 글

[23.11.22]  (1) 2023.11.22
[23.11.21]  (0) 2023.11.21
[23.11.17]  (0) 2023.11.17
[23.11.16]  (0) 2023.11.16
[23.11.15]  (3) 2023.11.15

오늘 배운 것

 Spring

- Paging & Sorting

JpaRepository 인터페이스의 부모 인터페이스 PagingAndSortingRepository에서는

Page<T> findAll(Pageable pageable);

이라는 페이징 + 소팅 메소드를 지원한다.

 

여기서 pageable에 여러가지 설정이 들어가는데 아래 코드를 보면 좋다.

 
Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC; 
Sort sort = Sort.by(direction, sortBy);

Pageable pageable = PageRequest.of(page, size, sort);//(현재 페이지, 노출될 페이지 수, 정렬방법)
//PageRequest.of(page, size, Sort.Direction.ASC, sortBy); 로도 가능

pageable의 구현체인 PageRequest로 현재 페이지, 노출될 페이지 수, 정렬방법이 들어가고 따로 sort를 지정하지 않고

PageRequest.of(page, size, Sort.Direction.ASC, sortBy(정렬될 카테고리)) 로 해도 된다.

 

이렇게 구성한 pageable은 쿼리 메소드도 지원해서 활용하면 아주 좋다.

Page<Product> products = productRepository.findAll(pageable);

Page<Product> products = productRepository.findAllByUser(user, pageable);

 

알고리즘

- 프로그래머스 : 하노이의 탑(Level 2, 재귀)

느낀 점

.

'TIL' 카테고리의 다른 글

[23.11.21]  (0) 2023.11.21
[23.11.20]  (0) 2023.11.20
[23.11.16]  (0) 2023.11.16
[23.11.15]  (3) 2023.11.15
[23.11.14]  (2) 2023.11.14

+ Recent posts