최신글
hyeonga_code
Project_04_Url 등록 기능 구현 본문
반응형
url 다이어리 서비스를 제공할 예정이므로 url 관련 기능을 구현한다.
1. Url Entity
package com.hyeongarl.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Size;
import lombok.*;
import org.hibernate.validator.constraints.URL;
import java.time.LocalDateTime;
/*
@Table(name="")
데이터베이스의 name에 지정된 이름의 테이블과 매핑된다.
@Entity
JPA 엔티티임을 명시하여 데이터베이스 테이블의 레코드와 매핑
@Getter, @Setter
롬복 라이브러리의 어노테이션으로 모든 필드에 대해 자동으로 getter/setter를 생성
@NoArgsConstructor
롬복 라이브러리의 어노테이션으로 매개변수가 없는 기본 생성자를 자동으로 생성
*/
@Entity
@Table(name="urls")
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Url {
/*
@Id
테이블의 기본 키
@GeneratedValue(strategy = GenerationType.IDENTITY)
기본 피의 생성 전략을 지정
IDENTITY : 데이터베이스가 기본 키 생성을 담당
기본적으로 AUTO_INCREMENT를 사용
@Column(name="user_id", updatable = false)
데이터베이스의 name과 일치하는 컬럼과 매핑된다.
필드가 수정될 수 없음을 의미
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long urlId;
@URL
@Column(name="url")
private String url;
@Size(max = 50)
@Column(name="url_title")
private String urlTitle;
@Size(max = 150)
@Column(name = "url_description")
private String urlDescription;
@Column(name = "category_id")
private Long categoryId;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "url_regdate")
private LocalDateTime urlRegdate;
@Column(name = "url_update")
private LocalDateTime urlUpdate;
@PrePersist // 등록 시 자동으로 실행
public void prePersist() {
this.urlRegdate = LocalDateTime.now();
}
@PreUpdate // 업데이트 시 자동으로 실행
public void preUpdate() {
this.urlUpdate = LocalDateTime.now();
}
}
2. UrlRequestDto
package com.hyeongarl.dto;
import com.hyeongarl.entity.Url;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UrlRequestDto {
@NotNull(message = "URL is required")
private String url;
@NotNull(message = "URL Title is required")
private String urlTitle;
private String urlDescription;
private Long categoryId;
public Url toEneity() {
return Url.builder()
.url(url)
.urlTitle(urlTitle)
.urlDescription(urlDescription)
.categoryId(categoryId)
.build();
}
}
3. UrlResponseDto
package com.hyeongarl.dto;
import com.hyeongarl.entity.Url;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UrlResponseDto {
private Long urlId;
private String url;
private String urlTitle;
private String urlDescription;
private Long categoryId;
private LocalDateTime urlRegdate;
private LocalDateTime urlUpdate;
public static UrlResponseDto fromEntity(Url url) {
return UrlResponseDto.builder()
.urlId(url.getUrlId())
.url(url.getUrl())
.urlTitle(url.getUrlTitle())
.urlDescription(url.getUrlDescription())
.categoryId(url.getCategoryId())
.urlRegdate(url.getUrlRegdate())
.urlUpdate(url.getUrlUpdate())
.build();
}
}
4. UrlService
package com.hyeongarl.service;
import com.hyeongarl.entity.Url;
import com.hyeongarl.error.UrlAlreadyExistException;
import com.hyeongarl.error.UrlInvalidException;
import com.hyeongarl.repository.UrlRepository;
import com.hyeongarl.util.UrlValidator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UrlService {
private final UrlRepository urlRepository;
public Url save(Url url) {
// url 유효성 검사
if(!UrlValidator.checkUrl(url.getUrl())) {
throw new UrlInvalidException();
}
// 이미 존재하는 정보(사용자, url())
if(urlRepository.existsByUrl(url.getUrl())) {
throw new UrlAlreadyExistException();
}
return urlRepository.save(url);
}
}
5. UrlRepository
package com.hyeongarl.repository;
import com.hyeongarl.entity.Url;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface UrlRepository extends JpaRepository<Url, Long> {
Page<Url> findAllByUserId(Pageable pageable, Long userId);
@Query("SELECT CASE " +
"WHEN COUNT(u) > 0 THEN true " +
"ELSE false END FROM Url u " +
"WHERE u.url = :url")
boolean existsByUrl(String url);
}
6. UrlController
package com.hyeongarl.controller;
import com.hyeongarl.config.jwt.TokenProvider;
import com.hyeongarl.dto.UrlRequestDto;
import com.hyeongarl.dto.UrlResponseDto;
import com.hyeongarl.entity.Url;
import com.hyeongarl.error.UnAuthorizedException;
import com.hyeongarl.service.UrlService;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("/url")
public class UrlController {
private final TokenProvider tokenProvider;
private final UrlService urlService;
@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public UrlResponseDto saveUrl(@RequestBody UrlRequestDto urlRequest, HttpServletRequest request) {
String token = request.getHeader("Authorization").substring(7);
Long userId;
try {
userId = tokenProvider.getUserId(token);
} catch (JwtException e) {
throw new UnAuthorizedException();
}
Url saveUrl = urlRequest.toEneity();
saveUrl.setUserId(userId);
return UrlResponseDto.fromEntity(urlService.save(saveUrl));
}
}
7. UrlInvalidException
package com.hyeongarl.error;
import org.springframework.http.HttpStatus;
public class UrlInvalidException extends BaseException {
private static final String DEFAULT_MESSAGE = "유효하지 않은 Url입니다.";
private static final String DEFAULT_CODE = "400003";
public UrlInvalidException() {
super(new ErrorResponse(DEFAULT_MESSAGE, DEFAULT_CODE), HttpStatus.BAD_REQUEST);
}
public UrlInvalidException(String message) {
super(new ErrorResponse(message, DEFAULT_CODE), HttpStatus.BAD_REQUEST);
}
}
8. UrlAlreadyExistsException
package com.hyeongarl.error;
import org.springframework.http.HttpStatus;
public class UrlAlreadyExistException extends BaseException {
private static final String DEFAULT_MESSAGE = "이미 존재하는 Url입니다.";
private static final String DEFAULT_CODE = "400007";
public UrlAlreadyExistException() {
super(new ErrorResponse(DEFAULT_MESSAGE, DEFAULT_CODE), HttpStatus.CONFLICT);
}
public UrlAlreadyExistException(String message) {
super(new ErrorResponse(message, DEFAULT_CODE), HttpStatus.CONFLICT);
}
}
UrlControllerTest
package com.hyeongarl.controller;
import com.hyeongarl.dto.UrlRequestDto;
import com.hyeongarl.dto.UrlResponseDto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UrlControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
@DisplayName("testSaveUrl")
void testSaveUrl() {
UrlRequestDto urlRequest = UrlRequestDto.builder()
.url("https://www.naver.com")
.urlTitle("saveUrlTitle")
.urlDescription("saveUrlDescription")
.categoryId(1L)
.build();
ResponseEntity<UrlResponseDto> responseEntity
= restTemplate.postForEntity("/url", urlRequest, UrlResponseDto.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED);
UrlResponseDto responseBody = responseEntity.getBody();
assertThat(responseBody).isNotNull();
assertThat(responseBody.getUrl()).isEqualTo("https://www.naver.com");
}
}
> 테스트 실행 오류1.
인증 처리가 되지 않아 오류가 발생했다.
테스트 시작시 회원 가입/로그인을 한 후 실행하려했으나 생성된 토큰을 설정하고 가져오지 못했고 모든 테스트에서 회원가입과 로그인을 진행해야 하는 번거로움을 없애기 위해 SecurityFilterChain 클래스의 filterChain() 메소드에서 모든 경로에 대해 엑세스를 누구나 가능하게 변경했다.
추후 로그인 인증으로 처리하는 값을 적용시킬 예정이다.
package com.hyeongarl.config;
import com.hyeongarl.config.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/**
configure : 스프링 시큐리티 기능 비활성화
filterChain : 특정 HTTP 요청에 대한 웹 기반 보안 구성
authenticationManager : 인증 관리자 관련 설정
bCryptPasswordEncoder : 비밀번호 인코더로 사용할 빈 등록
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityFilterChain { // SecurityConfig
private final TokenProvider tokenProvider;
...
@Bean // 특정 HTTP 요청에 대한 웹 기반 보안 구성 (인증, 인가, 로그인, 로그아웃 관련 설정)
public org.springframework.security.web.SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
TokenAuthenticationFilter tokenFilter = new TokenAuthenticationFilter(tokenProvider);
return http
.authorizeHttpRequests(
// 특정 경로에 대한 엑세스 설정
auth -> auth.requestMatchers( // 특정 요청과 일치하는 url에 대한 엑세스 설정
new AntPathRequestMatcher("/**")
).permitAll() // 누구나 접근 가능하게 설정
.anyRequest() // 위에서 설정한 url 이외의 요청에 대해 설정
.authenticated() // 별도의 인가는 필요하지 않으나 인증이 성공한 상태만 접근이 가능
)
.csrf(AbstractHttpConfigurer::disable) // CSRF 공격 방지를 위해서는 활성화하는게 좋음
.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class) // JWT 필터 추가
.build();
}
...
}
> UrlControllerTest / testSaveUrl() 실행
하루를 소비했다..
UrlServiceTest
package com.hyeongarl.service;
import com.hyeongarl.dto.UrlRequestDto;
import com.hyeongarl.entity.Url;
import com.hyeongarl.error.UrlAlreadyExistException;
import com.hyeongarl.error.UrlInvalidException;
import com.hyeongarl.repository.UrlRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/*
@ExtendWith(MockitoExtension.class)
JUnit5의 확장 모델을 활용하여 Mockito를 사용할 수 있다.
MockExtension : Mockito에 대한 설정을 초기화 및 @Mockito 처리
*/
@ExtendWith(MockitoExtension.class)
public class UrlServiceTest {
/*
@Mock
Mockito를 사용해 가짜 객체를 생성
*/
@Mock
private UrlRepository urlRepository;
/*
@InjectMocks
실제 객체로 만들어 필요한 의존성 객체를 주입
*/
@InjectMocks
private UrlService urlService;
@Test
@DisplayName("save_success")
void saveUrl_success() {
UrlRequestDto urlRequest
= new UrlRequestDto("https://www.naver.com", "saveUrlTitle", "saveUrl_Description", 1L);
when(urlRepository.existsByUrl(urlRequest.getUrl())).thenReturn(false);
when(urlRepository.save(any(Url.class))).thenReturn(urlRequest.toEntity());
Url url = urlService.save(urlRequest.toEntity());
assertNotNull(url);
assertEquals(urlRequest.getUrl(), url.getUrl());
assertEquals(urlRequest.getUrlTitle(), url.getUrlTitle());
assertEquals(urlRequest.getUrlDescription(), url.getUrlDescription());
verify(urlRepository, times(1)).save(any(Url.class));
}
@Test
@DisplayName("save_fail_invalid")
void saveUrl_fail_invalid() {
UrlRequestDto urlRequest
= new UrlRequestDto(null, "saveUrlTitle", "saveUrlDescription", 1L);
assertThrows(UrlInvalidException.class, () -> urlService.save(urlRequest.toEntity()));
verify(urlRepository, never()).save(any(Url.class));
}
@Test
@DisplayName("save_fail_alreadyExist")
void saveUrl_fail_alreadyExist() {
UrlRequestDto urlRequest
= new UrlRequestDto("https://www.naver.com", "saveUrlTitle", "saveUrl_Description", 1L);
when(urlRepository.existsByUrl(urlRequest.getUrl())).thenReturn(true);
assertThrows(UrlAlreadyExistException.class,
()-> urlService.save(urlRequest.toEntity()));
verify(urlRepository, never()).save(any(Url.class));
}
}
> 실행
반응형
'Project_HYEONGARL' 카테고리의 다른 글
Project_06_SpringSecurity + JWT 인증 후 다른 기능 구현 테스트 코드 작성 (0) | 2024.06.01 |
---|---|
Project_05_Url 조회 기능 구현 /TestCode (0) | 2024.06.01 |
Project_03_Spring Security, JWT 구현 (0) | 2024.05.30 |
Project_02_예외 처리하기 @ControllerAdvice, @RestControllerAdvice, @ExceptionHandler (0) | 2024.05.30 |
Project_01_Spring Boot Gradle Multi Module Project 생성하기 (0) | 2024.05.28 |