SMALL

Spring Security 관련 고민

Spring security에 JWT를 도입하였다. Jwt Authentication Filter 구성 시 SecurityContextHolder에 Member 객체를 전달해야 했다.

 

Jwt는 OncePerRequest로 설정되어 있었기에 모든 요청마다 Member 객체를 ContextHolder에 전달하려다 보니 매 요청마다 토큰 validation과 DB 조회를 통한 사용자 validation이 동시에 발생하였다.

 

첫번째 결론

DB 조회를 최소화 해야 시스템의 안정성과 성능 상의 오버헤드를 줄일 수 있다고 판단했다.

그에 따라 auth filter의 역할을 JWT 자체에 대한 validation으로 한정하고 사용자 validation은 controller 단에서 처리하고, 사용자 validation을 담당하는 component를 구성하여 사용하면 OOP 관점에서 SRP 원칙을 고수할 수 있겠다고 생각했다.

 

component를 사용하는 것보다 AOP를 지향하는 것은 어떨까 생각하였으나 Annotation 기반 설계는 자동화에는 매우 용이하나 method 보다 활용도가 떨어졌다.

 

프로젝트 특성상 엔티티(테이블)의 구조가 계층적이었고, 엔티티에 따라 데이터 사용 권한 validation 방식이 상이했다.

 

결국 Jwt auth filter에서 SecurityContextHolder를 사용할 수 밖에 없고, 이는 내가 생각했던 auth filter의 SRP 원칙에 어긋났다.

 

결론적으로,

1. AuthUtil 컴포넌트로 사용자 validation 역할을 분리했다.

2. JwtAuthenticationFilter에서는 토큰 자체의 Validation만 진행하기로 하고 JWT에 필요한 모든 정보를 포함하기로 했다.

3. SecurityContextHolder에는 실제 Member 객체가 아닌 JWT에 포함된 정보만 갖는 MemberClaim 객체를 사용하기로 했다.

 

두번째 결론

사용자의 데이터 소유권 validation에 DB 조회는 애초에 필요 없다는 것을 깨달았다.

 

JWT는 서버에서 generate 되는데, 그럼 DB 조회 후 생성된 Member 객체의 UUID는 JWT의 subject에 포함될 것이고, JWT verify를 통과했다면 변조 되지 않음이 보장되므로 당연히 유효한 사용자의 UUID일 것이다(로그아웃이나 토큰 Replay와 같은 예외는 별개의 문제)

 

결론적으로,

1. AOP 지향적으로 사용자 validation을 설계하고 현재 인증된 사용자에 대해선 Service 레이어에서 조회를 할 것이므로 새로운 컴포넌트는 불필요하다.

 

세번째 결론

사용자의 아이템 ID의 경우 DB 조회 및 join 등 복잡한 validation 로직이 필요한데 이의 경우는 AOP로 처리하기엔 한계가 있었다.

 

엔티티가 추가됨에 따라 사용자 권한 validation을 위한 aspect 코드와 새로운 annotation을 정의하는 것은 확장성과 유지보수성에 치명적이었다.

계층적인 구조를 띄는 예제 테이블

 

그래서 다소 우아하진 않지만 서비스별로 해당 서비스가 주로 다루는 엔티티에 관한 데이터 소유권 validation을 진행하는 private 메서드를 정의했다. 차후 리팩토링 시, 이들을 모아서 util 컴포넌트로 모아 OOP 원칙을 고수할 수 있도록 해야겠다.

 

결론적으로,

Service 단에서 소유권 Validation 로직을 다뤄야 한다.

네번째 결론

@PreAuthorize, @PostAuthorize, @Secured 어노테이션의 존재를 깨달았다.

@PreAuthorize를 통해 AOP 기반으로 메서드 실행 전 사용자의 해당 데이터에 대한 소유권을 먼저 검증한 다음 로직을 진행할 수 있다.

...
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig { ... }

 

해당 어노테이션을 사용하기 위해선 SecurityConfig에 다음 어노테이션을 추가한다

public abstract class AccessHandler {
    public final boolean ownershipCheck(UUID resourceId) {
        return isResourceOwner(resourceId);
    }

    public final UUID getCurrentMemberId() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return ((Member) auth.getPrincipal()).getId();
    }
    abstract boolean isResourceOwner(UUID resourceId);
}

 

@Component
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class VocabAccessHandler extends AccessHandler {
    private final VocabRepository vocabRepository;

    @Override
    boolean isResourceOwner(UUID resourceId) {
        return vocabRepository.findById(resourceId)
                .map(Vocab::getMember)
                .filter((m) -> getCurrentMemberId().equals(m.getId()))
                .isPresent();
    }
}

AccessHandler abstract를 정의하고 각 Entity에 맞게 검증 로직을 구현한다


    @Transactional(readOnly = true)
    @PreAuthorize("@vocabAccessHandler.ownershipCheck(#vId)")
    public VocabDto getVocabById(UUID vId) {
        Vocab vocab = vocabRepository.findById(vId)
                .orElseThrow(() -> new ApiException(NO_VOCAB));

        return VocabDto.fromEntity(vocab);
    }

데이터에 대한 트랜재션이 2번 중복되어 효율은 떨어질 수 있으나 단 하나의 조회를 늘림으로써 클린코드, 검증과 비즈니스 로직의 역할 분리, AOP 패턴을 사용할 수 있으니 이 정도의 오버헤드는 나쁘지 않은 trade-off 라고 생각된다

LIST
SMALL

내가 알고 있던 테스트에 관하여

기존에 유닛테스트가 중요하다는 것은 전공에서도 배웠고 여기저기서 주워들은 건 있었지만 정작 프로젝트를 진행하다보면 유닛테스트가 정말 의미가 있나 싶은 순간들이 많았다.

 

아래의 회원가입 코드의 경우, 스프링 DI로 부터 주입받은 다른 컴포넌트 로직의 동작을 제외하면 해당 메서드의 온전한 로직은 겨우 파라미터로 변수를 전달하거나 return 값을 던져주는 것 뿐이다.

    @Transactional
    public JoinDto.Response join(@Valid JoinDto.Request request) {
        Member member = Member.builder()
                .email(request.getEmail())
                .nickname(request.getNickname())
                .password(passwordEncoder.encode(request.getPassword()))
                .build();

        return JoinDto.Response.fromEntity(memberRepository.save(member));
    }

실제 로직의 코드에 비해 테스트 코드 사이즈는 아래와 같이 비대해진다.

@ExtendWith(MockitoExtension.class)
class TestTest {
    @Mock
    private MemberRepository memberRepository;

    @Mock
    private PasswordEncoder passwordEncoder;

    @InjectMocks
    private AuthService authService;

    @Test
    void joinTestSuccess() {
        // given
        JoinDto.Request request = JoinDto.Request.builder()
                .email("test@example.com")
                .nickname("test")
                .password("test123")
                .build();

        String encodedPassword = "encodedPassword123";

        Member savedMember = Member.builder()
                .email(request.getEmail())
                .nickname(request.getNickname())
                .password(encodedPassword)
                .build();

        when(passwordEncoder.encode(request.getPassword()))
                .thenReturn(encodedPassword);

        when(memberRepository.save(any(Member.class)))
                .thenReturn(savedMember);

        // when
        JoinDto.Response response = authService.join(request);

        // then
        assertEquals(request.getEmail(), response.getEmail());
        assertEquals(request.getNickname(), response.getNickname());
    }
}

모든 의존성을 전부 Mocking 하다보면 Java의 OOP 특성이 많이 드러나는 Spring에선 특히 테스트 코드 작성에 들이는 시간적인 오버헤드가 크다.

 

많은 모듈과 컴포넌트가 계층적으로 나눠져 의존성 주입을 통해 많은 것이 동작하는 스프링부트 아키텍처 특성 상 일일이 모든 모듈에 대한 테스트 코드를 작성하는 것은 배보다 배꼽이 더 큰 꼴이었기에 의문이 꽤 많이 들었다.

스프링의 계층적인 아키텍처 구조

Martin Fowler의 유닛 테스트

그래서 검색을 하던 중 ‘마틴 파울러’ 라는 사람의 테스트 피라미드 모델을 보게 되었고, 유닛 테스트에 대한 회의감이 심해지던 와중에 유닛 테스트의 비중이 가장 크다는 그림을 보고 더 혼란스러워졌다.

테스트 피라미드

아무리 봐도 자바는 체계적인 언어여서 통합 테스트만 진행하더라도 탄탄한 예외처리를 통해 충분히 빠른 디버깅이 가능할텐데 유닛 테스트의 중요성을 재차 강조하다니.

 

그러나 마틴 파울러가 말하는 유닛 테스트는 내가 알던 것과 약간 달랐다.

 

마틴 파울러가 말하는 유닛 테스트에는 Sociable(협동) Unit Test, Solitary(단일) Unit Test 두 가지가 존재했는데, 전자가 보통 흔히 생각하는 통합 테스트, 후자가 우리가 익히 배워왔던 고전적인 유닛 테스트였던 것이다.

 

즉, Solitary Unit Test는 mock/stub을 이용하여 해당 컴포넌트의 의존성을 배제하고 로직만을 테스트하는 것, Sociable Unit Test는 한 컴포넌트의 비즈니스 로직에 대해 다른 협력 컴포넌트들과 함께 테스트 한다는 것이다.

그렇다면 Sociable Test와 Integration Test의 차이는 무엇인가?

Sociable Test는 Unit test인 만큼 여전히 특정 컴포넌트나 모듈, 클래스에 대한 테스트라면, 통합 테스트는 아예 다른 시스템과의 상호작용이나, 다른 기술 스택 간의 상호작용, 외부 API, 네트워크 등의 영향을 받는 조건에 대해 모두 테스트 하는 것이다.

 

또한 Unit test 케이스는 개발자가 직접 작성한다면, Integration test 케이스는 개발자가 아닌 타인이 작성한다는 것이 차이점이다.

 

그렇다면 내게 필요한 것은 비즈니스 로직의 핵심적이고 복잡한 모듈에 대해선 Solitary Unit Test가 필요할 것이고 (예를 들자면 JWT 토큰을 생성하거나 Validation 하는 로직), 어느 정도 흐름이 필요한 모듈엔 Sociable Unit Test를 통해 해당 컴포넌트와 협력하는 다른 컴포넌트들과의 흐름을 테스트해야 할 것이다. (특정 Util 컴포넌트를 사용한 사용자 Validation 등)

Solitary(단독) Unit Test

흔히 생각할 수 있는 고전적인 방식의 Unit Test, 테스트더블(Mock, Stub)을 이용해 의존성을 배제하여 다른 컴포넌트의 영향을 받지 않고 오로지 해당 컴포넌트의 로직만을 테스트하는 것

Sociable(협동) Unit Test

특정 컴포넌트를 테스트 한다는 것은 고전적인 Unit Test와 동일하지만 의존성을 배제하지 않고 팀 또는 개발자가 지정한 Unit의 범위에 맞게 다른 컴포넌트와 함께 협동으로 테스트하는 것, 흐름 단위로 Unit의 범위를 지정할 수도 있음

 

통합 테스트와의 차이로는 통합테스트는 다른 시스템과의 상호작용이나, 다른 기술 스택 간의 상호작용, 외부 API, 네트워크 등의 영향을 받는 조건에 대해 모두 테스트 하는 것, 또한 Unit test 케이스는 주로 개발자가 직접 작성한다면, Integration test 케이스는 개발자가 아닌 타인이 작성할 수 있다는 것이 차이점

 

Sociable Unit Test

테스트 하고자 하는 Unit 범위 내에 있는 Bean은 실제 Bean을 사용하고, 범위를 벗어나는 컴포넌트는 Mocking을 하여 Unit Test를 진행할 수 있다

 

아래 코드는 JWT를 적용한 회원가입 성공 케이스에 대한 테스트이다. AuthService의 join() 메서드에 대한 전반적인 로직의 흐름을 테스트 하며 DB 접근에 대한 부분은 Mocking 하였다

 

- @TestPropertySource를 통해 application.properties를 Mocking 한다

- @ContextConfiguration을 통해 해당 테스트 Context 내에서 사용할 Bean의 형태를 지정한다

 --> AuthService.class는 @Service를 통해 정의되었기에 Spring Context를 통해 의존성이 주입되므로 별도로 지정한다

 --> 나머지는 생성자를 통해 객체를 생성할 수 있는 @Component 또는 @Bean 이므로 

- 테스트 클래스 내에 @TestConfiguration 어노테이션을 사용하여 어느 Bean을 Mocking하고 어느 Bean을 실제 Bean으로 사용할 지에 대한 의존성 주입 생성자를 지정한다

 --> @TestConfiguration은 @Configuration과 유사하나 Spring Test 전용으로 사용한다

@ExtendWith(SpringExtension.class)
@TestPropertySource(properties = {
        "jwt.secret=test-secret",
        "jwt.exp=3600"
})
@ContextConfiguration(classes = {AuthService.class, AuthServiceTest.TestConfig.class})
class AuthServiceTest {

    @Autowired
    private AuthService authService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private MemberRepository memberRepository; // mock임

    @Test
    void joinTestSuccess() {
        // given
        String rawPassword = "test1234";
        String encodedPassword = passwordEncoder.encode(rawPassword);

        JoinDto.Request request = JoinDto.Request.builder()
                .email("user@example.com")
                .nickname("tester")
                .password(rawPassword)
                .build();

        Member memberToReturn = Member.builder()
                .email(request.getEmail())
                .nickname(request.getNickname())
                .password(encodedPassword)
                .build();

        // mocking
        Mockito.when(memberRepository.save(Mockito.any(Member.class)))
                .thenReturn(memberToReturn);

        // when
        JoinDto.Response response = authService.join(request);

        // then
        assertEquals(request.getEmail(), response.getEmail());
        assertEquals(request.getNickname(), response.getNickname());
        Mockito.verify(memberRepository).save(Mockito.any(Member.class));
    }

    @TestConfiguration
    static class TestConfig {

        @Bean
        public MemberRepository memberRepository() {
            return Mockito.mock(MemberRepository.class);
        }

        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder(); // 실제 컴포넌트 사용
        }

        @Bean
        public JwtProperties jwtProperties() {
            return new JwtProperties();
        }

        @Bean
        public JwtUtil jwtUtil() {
            return new JwtUtil(jwtProperties());
        }

        @Bean
        public JwtBlacklistService jwtBlacklistService() {
            return Mockito.mock(JwtBlacklistService.class);
        }

        @Bean
        public AuthenticationManager authenticationManager() {
            return Mockito.mock(AuthenticationManager.class);
        }
    }
}

 

LIST
SMALL

Refactoring Tips

  • 변경되면 안되는 변수나 RequestBody 파라미터에 final 붙이기
  • 중복되거나 길어지는 코드 private 지역 변수 선언하기
  • Null이면 안되는 Parameter @Nonnull 키워드 붙이기
  • get 메서드는 없으면 Throw 날리기, find 메서드는 애초에 Optional로 반환하는 것이 좋음
  • Magic Number Validation 코드 줄이는 방법!
    • final 변수로 상수 값 갖는 클래스 만들기 (DMakerConstant.class)
    • Validation 할 항목의 Enum에 상수로 최소, 최대값 할당해주기
    • Function<>() 이용해서 Enum 자체에 Validation 메서드 만들기 (진짜 개천재인 듯)
    • @AllArgsConstructor
      @Getter
      public enum DeveloperLevel {
          NEW("신입 개발자", years -> years == 0),
          JUNIOR("주니어 개발자", years -> years <= MAX_JUNIOR_EXPERIENCE_YEAR),
          JUNGNIOR("중니어 개발자", years -> years > MAX_JUNIOR_EXPERIENCE_YEAR
                  && years < MIN_SENIOR_EXPERIENCE_YEAR),
          SENIOR("시니어 개발자", years -> years >= MIN_SENIOR_EXPERIENCE_YEAR);
      
          private final String description;
          private final Function<Integer, Boolean> validateFunction;
      
          public void validateExperienceYears(Integer years){
              if(!validateFunction.apply(years))
                  throw new DMakerException(DMakerErrorCode.LEVEL_EXPERIENCE_YEARS_NOT_MATCHED);
          }
      }
      
LIST
SMALL
  • 스프링의 표현식으로 스프링의 모든 영역에서 사용 가능
  • @Value(”$(config.value)”) 처럼 사용
  • SpEL의 값 Evaluation
    • Spellparser는 “” 안에 있는 문자열을 Eval 해서 결과값을 만들어냄
    • String 객체를 new로 생성하여 사용할 수도 있음
    • ExpressionParser parser = new SpelExpressionParser();
      Expression exp = parser.parseExpression("'Hello World'");
      String message = (String) exp.getValue(); //"Hello World"
      
      Expression expWow = 
      	parser.parseExpression("'Hello world'.concat('!')");
      String messageWow = (String) expWow.getValue(); //"Hello World!"
      
      Expression expString =
      	parser.parseExpression("new String('hello world').toUpperCase()");
      String messageString = expString.getValue(String.class); //"HELLO WORLD"
      
  • Bean의 Property를 설정할 때 사용하는 방식
    • 보통 직전의 방식 보다는 아래와 같이 사용함
    • 기본적으로 #{<표현식>} 이렇게 사용
    • application.properties(또는 application.yml)의 값을 가져올 땐 ${<property 이름>} 방식으로 가져옴 ex) CPU의 개수 등
    • 테스트 환경과 운영 환경을 분리하고 싶을 때도 사용
    @Component
    public class SimpleComponent {
    	@Value("#{ 1+1 }")
    	int two; // 2
    
    	@Value("#{ 2 eq 2}")
    	boolean isTrue; // true
    
    	@Value("${ server.hostname }")
    	String hostName; // www.server.com
    
    	@Value("#{ ${ server.hostname } eq 'www.server.com'}")
    	boolean isHostSame; // true
    
    
LIST
SMALL
  • java.net.URL의 한계(classpath 내부 접근이나 상대경로 등)를 넘어서기 위해 추가적으로 구현된 부분
  • 실무에서 딱히 필수적이진 않음
  • 구현체 목록
    • UrlResource : “ftp:”, “file:”, “http:” 등의 prefix로 Resource에 접근, 기본적으로는 http(s): 로 접근
    • ClassPathResource : 코드를 빌드한 결과가 특정 경로로 저장되는데 이 때 하위 파일들을 접근하기 위해 사용
    • FileSystemResource : 로컬의 파일을 경로로 특정해 사용
    • SevletContextResource : Spring 밖의 Sevlet 어플리케이션 루트의 리소스에 접근
    • InputStreamResource : 키보드, 마우스 등 Input 값 리소스에 접근
    • ByteArrayResource : Byte로 들어오는 Stream을 읽는 용도
  • Spring ResourceLoader
    • 스프링 프로젝트 내의 특정 리소스에 접근할 때 사용, 대부분의 사전 정의된 파일들은 자동으로 로딩이 되나 추가적으로 필요한 파일이 있을 때 이 기능을 사용
    • 기본적으로 ApplicationContext에 구현되어 있다
    @Service
    public class ResourceService {
    	@Autowired
    	ApplicationContext ctx;
    
    	public void setResource() {
    		Resource myTemplate = 
    			ctx.getResource("classpath:some/resource/path/myTemplate.txt");
    	}
    }
    
  • ResourcePatternResolver
    • ApplicationContext에서 ResourceLoader를 불러올 때 사용하는 Interface, 위치 지정자 패턴에 따라 자동으로 Resource Loader 구현체를 선택
  • Application Contexts & Resource Paths
    • applicationContext(스프링의 핵심 설정)을 이루는 설정 값을 가져오는 방법 들
    • 예전엔 xml로 Configuration을 하였으나 요즘은 대부분 Java Config로, Annotation 기반의 설정으로 바뀐다고 함
    • ApplicationContext ctx = 
      	new ClassPathXmlApplicationContext("conf/appContext.xml");
      
      ApplicationContext ctx = 
      	new FileSystemXmlApplicationContext("conf/appContext.xml");
      
      //use ctx as a spring
      Bear bear = (Bear) ctx.getBean("bear");
      
LIST
SMALL

Request의 내용에 잘못된 내용이 있는지 확인하는 작업

  • 유형
    • 데이터 검증
      • 필수 데이터의 존재 유무
      • 문자열의 길이나 숫자형 데이터의 값의 범위 등
      • email, 신용카드 번호 등 특정 형식에 맞춘 데이터
    • 비즈니스 검증
      • 서비스 정책에 따른 데이터 검증
      • ex) 배달 요청을 할 때 해당 주문건이 결제 완료 상태인지 확인 등
      • 경우에 따라 외부 API 호출, DB 데이터 조회하여 검증하는 등
  • Spring의 Validation
    • 스프링의 경우 웹 레이어에 종속적이지 않은 방법으로 Validation을 하길 유도함
    • Java Bean Validation
      • 해당 방법이 좀 더 선호됨
      • JavaBean 내에 어노테이션으로 검증방법을 명시
        • Request DTO에 아래처럼 어노테이션 명시
        • public class MemberCreationRequest{
          	@NotBlank(message="이름을 입력해주세요.")
          	@Size(max=64, message="이름의 최대 길이는 64자 입니다.")
          	private String name;
          	@Min(0, "나이는 0보다 커야 합니다.")
          	private int age;
          	@Email("이메일 형식이 잘못되었습니다.")
          	private int email;
          	
          	//getter setter 등 ..
          }
          
        • 해당하는 @RequestBody에 @Valid 어노테이션을 달게 되면 Method 수행 전 Validation을 수행하게 됨
    • Spring validator 인터페이스
      • 특정 클래스 인스턴스에서만 동작하는 클래스를 Validator 인터페이스를 구현하여 사용
      • JavaBean Validation에 비해 조금 더 복잡한 검증이 가능하나 코드 확인이 어려울 수 있고 비즈니스 검증 로직이 여러 군데 흩어질 수 있기 때문에 잘못된 검증(중복 검증, 다른 정책을 따르는 검증)을 수행할 가능성이 높아짐
      • supports 메서드 : validator가 동작할 조건을 정의, 주로 class의 타입을 비교
      • validate : 원하는 validation을 진행
      public class PersonValidator implements Validator {
      	/*
      		This Validator validates only Person instances
      	*/
      	public boolean supports(Class clazz){
      		return Person.class.equals(clazz);
      	}
      	
      	public void validate(Object obj, Errors e){
      		ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
      		Person p = (Person) obj;
      		if(p.getAge() < 0){
      			e.rejectValue("age", "negativevalue");
      		}else if(p.getAge() > 110){
      			e.rejectValue("age", "too.darn.old");
      		}
      	}
      }
      
    • 유의사항
      • validation이 여러 군데에 흩어져있으면 테스트 및 유지보수성 감소
        • 중복된 검증, 다른 검증 등
      • 가능한 한 validation은 로직 초기에 수행 후 실패 시 exception을 throw 하는 편이 처리가 편리함
    • 실무 사용 패턴
      • 요청 dto에서 Java Bean Validation으로 단순 데이터(유무, 범위, 형식 등)을 1차 검증
      • 로직 초기에 2차로 비즈니스 검증 수행 후 실패 시에는 Custom Exception으로 예외 Throw 하여 응답 생성
LIST
SMALL

Data Binding

  • 사용자나 외부 서버의 요청 데이터를 특정 도메인 객체에 저장하여 프로그램에 Request에 담아주는 것
  • Converter
    • Json 형식으로 되어있는 String 타입의 데이터를 변환하여 특정 클래스로 변환하여 처리함
    • Converter를 만들어서 Bean으로 등록, 스프링에 내장된 서비스인 ConversionService에서 Converter 구현체 Bean들을 Converter 리스트에 등록
    • ConversionService는 모든 들어오는 데이터를 Conversion 해주는 서비스, Converter 리스트에 있는 Converter를 스캔하여 데이터를 자동으로 변환
    • Converter<S, T> Interface
      • S라는 타입을 받아 T라는 타입으로 변환해주는 Interface
      • package org.springframework.core.convert.converter;
        
        public interface Converter<S, T> {
        	T convert(S source);
        }
  • Formatter
    • 특정 객체 ↔ String 간의 변환 담당
    • Request의 데이터를 변환하는 Converter와 다르게 Response 또한 변환할 수 있음(데이터 → String)
    • Conveter와 마찬가지로 ConversionService에 등록하여 사용하며 요청/응답 시 해당 데이터 타입이 있는 경우 자동으로 동작함
    • package org.springframework.format.datetime;
      
      public final class DateFormatter implements Formatter<Date> {
      	public String print(Date date, Locale locale){
      		return getDateFormat(locale).format(date);
      	}
      	
      	public Date parse(String formatted, Locale locale) throws ParseException {
      		return getDateFormat(locale).parse(formatted);
      	}
      }
LIST
SMALL

스프링 부트의 기본적인 아키텍처

https://images.app.goo.gl/pgxUSYdQUYLr47m78

  • Controller : 사용자 인터페이스, HTTP Request, Response를 처리하며 입력값 검증 등을 필요로 함. 요청이 들어올 경우 Service에서 로직을 호출해 처리함
  • Service : 비즈니스 로직, Controller에서 요청을 받으면 Service에서 로직으로 처리함. 데이터는 DAO(Repository)를 통해 CRUD를 진행함. Controller와 데이터를 주고 받을 때 DTO(Data Transfer Object)로 변환하여 캡슐화. Repository와는 Entity로 데이터를 주고 받음
  • Repository : DB에 접근하기 위한 메서드들을 가진 인터페이스
  • DAO(Repository) : Date Acess Object, DAO Interface와 repository 객체를 통해 DB와 상호작용. Spring에선 JPA가 이를 담당
LIST
SMALL

REST(Representational State Transfer)

리소스를 이름으로 구분하여 해당 리소스의 상태를 주고 받는 모든 것

  • HTTP 프로토콜의 표준과 인프라를 그대로 사용하므로 별도의 추가적인 인프라 구축 없이 범용적으로 사용 가능
  • 단, 구형 브라우저에서는 호환되지 않음 (Explorer)
  • HTTP URI(Uniform Resource Identifier)를 통해 리소스를 명시
  • HTTP Method(POST, GET, PUT, DELETE, PATCH 등)을 통해 해당 자원(URD)에 대해 CRUD Operation을 적용
  • CRUD : Create(POST), Read(GET), Update(PUT, PATCH), Delete(DELETE)
  • 특성
    • Server-Client 구조
    • Stateless(무상태)
    • Cacheable(캐시 처리 가능)
    • Layered System(계층화)
    • Uniform Interface(인터페이스 일관성)

RESTful

REST의 원리를 따르는 시스템, REST 원리를 모두 적용하지 않았다면 Restful이라고 할 수 없다

LIST

+ Recent posts