no image
ApplicationContext - Annotation 기반 Bean register 등록 원리
Spring Context Bean이 등록되는 과정을 한번 보자 우선 ApplicationContext의 구현체인 AnnotationConfigApplicationContext를 보자 이 구현채는 기존 xml로 하나하나 빈으로 등록해주던 동작을 @Configuration 어노테이션이 적용된 자바 설정 파일을 통해 빈을 등록이 가능하도록 해준 클래스이고 @Component, @Service, @Repository, @Controller와 같은 어노테이션이 붙은 클래스들을 자동으로 감지하여 빈으로 등록할 수 있도록 해준다. AnnotationConfigApplicationContext는 Spring의 기본 컨테이너인 ApplicationContext의 구현체로, 자바 설정 파일을 기반으로 동작합니다. 이 컨..
2024.10.23
no image
[Spring] Maven과 Gradle
Spring 프로젝트를 시작할 때 빌드 도구를 Maven과 Gradle 중 하나를 선택해서 진행해야 한다. 예전에 스프링을 처음 배울 때는 Maven을 사용하는 사람들이 꽤 많았다. 그러나 요즘은 대부분이 Gradle을 사용한다. 이 두 종류의 빌드도구를 알아보기 전에 빌드 도구에 대해서 먼저 알아보자. 🛠빌드도구 프로젝트에서 작성한 자바 코드와 프로젝트 내에 필요한 각종 설정 파일들을 JVM이나 WAS가 인식할 수 있도록 패키징 해주는 과정을 빌드 도구라고 한다. 예를 들면 어떤 라이브러리를 사용할 때, 만약 빌드 도구가 없다면 번거롭게 개발자가 모두 다운받아서 사용하여야 한다. 그러나 빌드도구가 있으면 설정 파일에 필요한 라이브러리 종류와 버전들, 종속성 정보를 명시하며 필요한 라이브러리들을 설정 파..
2022.01.06
no image
[Spring] JPA+Spring으로 카테고리 로직 구현 - 3
🛠Controller 마지막으로 Api 컨트롤러를 구현할 것이다. RestController를 사용할 것이다. 전통적인 Spring MVC의 컨트롤러인 @Controller는 주로 View를 반환하기 위해 사용한다. @RestController는 Spring MVC Controlle에 @ResponseBody가 추가된 것이다. 당연하게도 RestController의 주용도는 Json 형태로 객체 데이터를 반환하는 것이다. ➡️SaveCategory @PostMapping public ResponseEntity saveCategory(@Valid@RequestBody SaveRequest request){ categoryService.createCategory(request); return OK; } 우선..
2021.12.10
no image
[Spring] JPA+Spring으로 카테고리 로직 구현 - 2
이번 포스팅은 Service를 구현하고 그에 대한 테스트 코드를 작성할 것이다. 우선 Service를 구현하기 전에 계층 간 데이터 전송을 하기 위해 먼저 DTO를 구현한다. 🧷DTO @Getter @NoArgsConstructor public static class SaveRequest{ private String categoryName; private String parentCategory; private Map subCategory; @Builder public SaveRequest(String categoryName, String parentCategory, Map subCategory) { this.categoryName = categoryName; this.parentCategory = par..
2021.12.10
no image
[Spring] JPA+Spring으로 카테고리 로직 구현 - 1
토이 프로젝트를 진행하면서 카테고리를 만드는데 무한 뎁스로 만드는 과정을 기록하려 한다. Stack : Java,Gradle, Spring data jpa ,Mysql 위 사진 처럼 유연하게 설계하여 무한하게 카테고리가 생성이 가능한 로직을 구현할것이다. 🔓요구조건 Entity 구현 하나의 테이블에서 구현해야하기 떄문에 Self Join 사용하여 구현 Repository 인터페이스 구현 Spring data JPA를 사용을 위해 JpaRepository를 상속 받는다. DTO 구현 Entity는 DB를 생성하고 DB를 가지고 계층간의 소통을 하기 위해 DTO를 구현할 것이다. Entity에서 list로 담은 childCategory는 DTO에서는 Map으로 구현한다. Service 구현 Save : 카테..
2021.12.10
[Spring] 어노테이션으로 로그인 여부 확인(Interceptor)
프로젝트 진행 중 컨트롤러의 메서드가 로그인 여부를 확인하는 부가기능을 동시에 처리하는 경우가 존재한다. 이런식으로 반복되는 로직을 AOP,Interceptor,Filter 등을 통해 반복되는 부가기능을 공통적으로 처리할 수 있다. 로그인 확인이 필요한 경우 프로젝트를 진행하다가 보면 로그인 없이 접근 가능한 페이지가 있고 로그인을 한 경우에만 접근이 가능한 페이지가 존재한다. 예를들어 마이페이지,비밀번호 변경,상품 등록하기 등이 있다. Interceptor를 적용하기 전 로그인 확인 과정 세션에서 현재 로그인된 사용자의 정보를 꺼내온다. 세션에서 꺼낸 값이 Null이라면 , 해당 사용자는 로그인을 하지 않은 상태이므로 401 UNAUTHORIZED를 반환한다. 만약 정상적으로 session에서 로그인 ..
2021.09.21
no image
[Spring] AOP(Aspect Oriented Programming)
📖 AOP(Aspect Oriented Programming) 개발을 하다보면 공통적인 기능이 많이 발생한다. 이러한 공통 기능을 모든 모듈에 적용하기 위해 상속을 하지만 자바에서는 다중상속이 불가능해서 상속을 받아 공통 기능을 부여하기에는 한계가 있다. AOP를 사용하지 않는다면 중복 코드가 발생할 소지가 있고, 코드의 변경이 필요하면 여러코드에 종속적으로 변경이 필요하고 핵심 비즈니스 로직에 호출 시간 측정이라는 부수적인 로직이 추가되어 가독성과 효율성이 떨어지는 문제가 발생한다. 이러한 문제점을 해결하기 이해서는 AOP가 필요하다.AOP는 새로운 프로그래밍 패러다임이 아니라 객체 지향 프로그래밍(OOP)를 돕는 보조적인 기술로, 핵심적인 관심사항과 공통 관심사항으로 분리 시키고 각각을 모듈화 하는 ..
2021.09.10
no image
ResponseEntity
➡️ResponseEntity란? SpringFrameword에서 제공하는 클래스 중 HttpEntity라는 클래스가 존재한다. 이것은 HTTP 요청(request) 또는 응답(response)에 해당하는 HttpHeader와 HttpBody를 포함하는 클래스이다. 1 2 3 4 5 6 7 public class HttpEntity { private final HttpHeaders headers; @Nullable private final T body; } Colored by Color Scripter cs 1 2 3 public class RequestEntity extends HttpEntity public class ResponseEntity extends HttpEntity cs ResponseE..
2021.08.25
728x90

Spring Context Bean이 등록되는 과정을 한번 보자 우선 ApplicationContext의 구현체인 AnnotationConfigApplicationContext를 보자

 

이 구현채는 기존 xml로 하나하나 빈으로 등록해주던 동작을 @Configuration 어노테이션이 적용된 자바 설정 파일을 통해 빈을 등록이 가능하도록 해준 클래스이고 @Component, @Service, @Repository, @Controller와 같은 어노테이션이 붙은 클래스들을 자동으로 감지하여 빈으로 등록할 수 있도록 해준다.

 

AnnotationConfigApplicationContext는 Spring의 기본 컨테이너인 ApplicationContext의 구현체로, 자바 설정 파일을 기반으로 동작합니다. 이 컨텍스트는 BeanFactory를 포함하여 빈의 라이프사이클을 관리해준다.

    public void register(Class<?>... componentClasses) {
        Assert.notEmpty(componentClasses, "At least one component class must be specified");
        StartupStep registerComponentClass = this.getApplicationStartup().start("spring.context.component-classes.register").tag("classes", () -> {
            return Arrays.toString(componentClasses);
        });
        this.reader.register(componentClasses);
        registerComponentClass.end();
    }

 

코드를 간단하게 설명하자면 스프링 애플리케이션이 시작되면 @Bean이 붙은 모든 클래스를 불러와서 register를 통해 Bean을 등록한다.

 

AnnotationConfigApplicationContext에서 받아온 Class들을 AnnotatedBeanDefinitionReader register를 통해 Bean에 등록하는 작업을 해준다.

 

이제 AnnotatedBeanDefinitionReader register 의 동작을 한번 보자 

public void register(Class<?>... componentClasses) {
     Class[] var2 = componentClasses;
     int var3 = componentClasses.length;

     for(int var4 = 0; var4 < var3; ++var4) {
         Class<?> componentClass = var2[var4];
          this.registerBean(componentClass);
    }
}

 

    public void registerBean(Class<?> beanClass) {
        this.doRegisterBean(beanClass, (String)null, (Class[])null, (Supplier)null, (BeanDefinitionCustomizer[])null);
    }

 

private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name, @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier, @Nullable BeanDefinitionCustomizer[] customizers) {
        AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
        if (!this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
            abd.setAttribute(ConfigurationClassUtils.CANDIDATE_ATTRIBUTE, Boolean.TRUE);
            abd.setInstanceSupplier(supplier);
            ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
            abd.setScope(scopeMetadata.getScopeName());
            String beanName = name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry);
            AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
            int var10;
            int var11;
            if (qualifiers != null) {
                Class[] var9 = qualifiers;
                var10 = qualifiers.length;

                for(var11 = 0; var11 < var10; ++var11) {
                    Class<? extends Annotation> qualifier = var9[var11];
                    if (Primary.class == qualifier) {
                        abd.setPrimary(true);
                    } else if (Lazy.class == qualifier) {
                        abd.setLazyInit(true);
                    } else {
                        abd.addQualifier(new AutowireCandidateQualifier(qualifier));
                    }
                }
            }

            if (customizers != null) {
                BeanDefinitionCustomizer[] var13 = customizers;
                var10 = customizers.length;

                for(var11 = 0; var11 < var10; ++var11) {
                    BeanDefinitionCustomizer customizer = var13[var11];
                    customizer.customize(abd);
                }
            }

            BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
            definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
            BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
        }
    }

 

AnnotationConfigApplicationContext에서 넘어온 클래스 목록이 doRegister에 의해 등록된다. 

코드를 보면서 내려오다 보면 Primary, Lazy가 보인다. Primary는 우선순위를 설정하는 것이고 Lazy는 애플리케이션 시작 때 생성이 아닌 필요시에 생성되기때문에 따로 설정을 해준다.

그 외에는 Qualifier 어노테이션 같은 것이 처리된다. AutowireCandidateQualifier 객체로 래핑하여 해당 어노테이션을 추가하고 이는 빈이 자동 주입될 때 어떤 조건으로 주입할지 결정하는 데 사용한다.

 

그리고 마지막줄 3줄을 보면 AnnotatedGenericBeanDefinition,ScopeMetadata,BeanDefinitionHolder가 있다.

무슨 역할을 하는지 보자

 

AnnotatedGenericBeanDefinition BeanDefinition의 구현체, Bean의 속성, 생성자 등의 정보를 갖는다.

ScopeMetadata는 Bean Scope의 정보이다.

BeanDefinitionHolder BeanDefinition와 name, alias 정보를 추가적으로 갖는다.

 

위 동작을 수행 후 BeanDefinitionReaderUtils의 registerBeanDefinition에 BeanDefinitionHolder와 registry를 인자로 메서드를 호출하면 Bean이 등록된다.

public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException {
    String beanName = definitionHolder.getBeanName();
    registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
    String[] aliases = definitionHolder.getAliases();
    if (aliases != null) {
        String[] var4 = aliases;
        int var5 = aliases.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            String alias = var4[var6];
            registry.registerAlias(beanName, alias);
        }
    }

}

 

메서드를 호출하면 넘어온 registry에 의해 등록된다. registry는 어떤 값인지 알아보자 

 

private final BeanDefinitionRegistry registry;

public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry) {
    this(registry, getOrCreateEnvironment(registry));
}

우선 BeanDefinitionReaderUtils의 registerBeanDefinition 호출 시에 넘어가는 registry에 어떤값이 들어가는지 보자

AnnotatedBeanDefinitionReader를 생성 할때 주입 받는다. 이건 아마 AnnotationConfigApplicationContext에서 생성자로 생성을 할때 registry를 넘겨주는듯 하다.

 

AnnotationConfigApplicationContext를 보면 생성자에서 자기 자신을 registry로 넣어서 AnnotatedBeanDefinitionReader를 생성한다. 

private final AnnotatedBeanDefinitionReader reader;


public AnnotationConfigApplicationContext() {
        StartupStep createAnnotatedBeanDefReader = this.getApplicationStartup().start("spring.context.annotated-bean-reader.create");
        this.reader = new AnnotatedBeanDefinitionReader(this);
        createAnnotatedBeanDefReader.end();
        this.scanner = new ClassPathBeanDefinitionScanner(this);
}

AnnotationConfigApplicationContext 생성자를 보면 AnnotatedBeanDefinitionReader 생성에 자기 자신(AnnotationConfigApplicationContext)을 인자로 한다.

 

이건 AnnotationConfigApplicationContext가 BeanDefinitionRegistry(GenericApplicationContext) 의구현체이기도 하기 때문이다. 

public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {
.
.
.
}

 

그럼 마지막으로 GenericApplicationContext에 registerBeanDefinition로직을 보자 

public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
    private final DefaultListableBeanFactory beanFactory;
    
    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
        this.beanFactory.registerBeanDefinition(beanName, beanDefinition);
    }
    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
        Assert.hasText(beanName, "Bean name must not be empty");
        Assert.notNull(beanDefinition, "BeanDefinition must not be null");
        if (beanDefinition instanceof AbstractBeanDefinition abd) {
            try {
                abd.validate();
            } catch (BeanDefinitionValidationException var8) {
                BeanDefinitionValidationException ex = var8;
                throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Validation of bean definition failed", ex);
            }
        }

        BeanDefinition existingDefinition = (BeanDefinition)this.beanDefinitionMap.get(beanName);
        if (existingDefinition != null) {
            if (!this.isBeanDefinitionOverridable(beanName)) {
                throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
            }

            if (existingDefinition.getRole() < beanDefinition.getRole()) {
                if (this.logger.isInfoEnabled()) {
                    this.logger.info("Overriding user-defined bean definition for bean '" + beanName + "' with a framework-generated bean definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
                }
            } else if (!beanDefinition.equals(existingDefinition)) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Overriding bean definition for bean '" + beanName + "' with a different definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
                }
            } else if (this.logger.isTraceEnabled()) {
                this.logger.trace("Overriding bean definition for bean '" + beanName + "' with an equivalent definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]");
            }

            this.beanDefinitionMap.put(beanName, beanDefinition);
        } else {
            if (this.isAlias(beanName)) {
                String aliasedName = this.canonicalName(beanName);
                if (!this.isBeanDefinitionOverridable(aliasedName)) {
                    if (this.containsBeanDefinition(aliasedName)) {
                        throw new BeanDefinitionOverrideException(beanName, beanDefinition, this.getBeanDefinition(aliasedName));
                    }

                    throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Cannot register bean definition for bean '" + beanName + "' since there is already an alias for bean '" + aliasedName + "' bound.");
                }

                this.removeAlias(beanName);
            }

            if (this.hasBeanCreationStarted()) {
                synchronized(this.beanDefinitionMap) {
                    this.beanDefinitionMap.put(beanName, beanDefinition);
                    List<String> updatedDefinitions = new ArrayList(this.beanDefinitionNames.size() + 1);
                    updatedDefinitions.addAll(this.beanDefinitionNames);
                    updatedDefinitions.add(beanName);
                    this.beanDefinitionNames = updatedDefinitions;
                    this.removeManualSingletonName(beanName);
                }
            } else {
                this.beanDefinitionMap.put(beanName, beanDefinition);
                this.beanDefinitionNames.add(beanName);
                this.removeManualSingletonName(beanName);
            }

            this.frozenBeanDefinitionNames = null;
        }

        if (existingDefinition == null && !this.containsSingleton(beanName)) {
            if (this.isConfigurationFrozen()) {
                this.clearByTypeCache();
            }
        } else {
            this.resetBeanDefinition(beanName);
        }

    }
    
  }

 

beanDefinitionMap에 beanName, beanDefinition을 저장한다.

 

이제 Bean 등록까진 되었으니 아마 다음은 Bean이 실제로 생성되는 부분을 뜯어보지 않을까싶다.

728x90

[Spring] Maven과 Gradle

ryudjae
|2022. 1. 6. 23:18
728x90

Spring 프로젝트를  시작할 때 빌드 도구를 Maven과 Gradle 중 하나를 선택해서 진행해야 한다. 예전에 스프링을 처음 배울 때는 Maven을 사용하는 사람들이 꽤 많았다. 그러나 요즘은 대부분이 Gradle을 사용한다. 

이 두 종류의 빌드도구를 알아보기 전에 빌드 도구에 대해서 먼저 알아보자.

 

🛠빌드도구

프로젝트에서 작성한 자바 코드와 프로젝트 내에 필요한 각종 설정 파일들을 JVM이나 WAS가 인식할 수 있도록 패키징 해주는 과정을 빌드 도구라고 한다.

예를 들면 어떤 라이브러리를 사용할 때, 만약 빌드 도구가 없다면 번거롭게 개발자가 모두 다운받아서 사용하여야 한다. 그러나 빌드도구가 있으면 설정 파일에 필요한 라이브러리 종류와 버전들, 종속성 정보를 명시하며 필요한 라이브러리들을 설정 파일을 통해 자동으로 다운로드해주고 관리까지 해주는 것이다.

 

⛏Maven

  • Maven은 프로젝트 빌드/관리를 위한 모듈이고, 자바 빌드 도구로서 Apache Ant를 대안으로 나온 것이다.
  • 프로젝트 객체 모델 POM의 개념을 기반으로 Maven은 중앙 정보에서 프로젝트의 빌드, 보고 및 문서를 관리한다.
  • 빌드 중인 프로젝트, 빌드 순서, 다양한 외부 라이브러리 종속성 관계를 pom.xml에 명시한다.
  • Maven은 외부 저장소에서 필요한 라이브러리와 플러그인들을 다운로드 한 다음 , 로컬 시스템의 캐시에 저장한다.

Maven의 설정 파일 Pom.xml 예제

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>com.example</groupId>
   <artifactId>demo-maven</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>demo-maven</name>
   <description>Demo project for Spring Boot</description>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.5.4.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
   </properties>

   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter</artifactId>
      </dependency>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>


</project>

 

⛏Gradle

  • Maven과 Ant의 장점을 조합해서 만든 빌드 도구이다.
  • 멀티 프로젝트 빌드를 도울 수 있도록 디자인되었다.
  • Gradle은 프로젝트가 어느 부분이 업데이트되었는지 알기 때문에, 빌드에 점진적으로 추가할 수 있다.

Gradle 설정 파일 build.gradle 예제

buildscript {
    ext {
        springBootVersion = '1.5.4.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'

version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter')
}

 

🪓Maven VS Gradle

우선 둘 중에는 Gradle이 확실히 우세한 것 같다. Gradle에 보면 Maven과 비교 문서가 있다. 뭐 그만큼 자신 있다는 거 아닐까,,,

그럼에도 불구하고 많은 사람들이 Maven에서 넘어오지 못한 이유는 익숙함 때문이 아닌가 싶다

  • Gradle이 더 늦게 출시했기 때문에 사용성 , 성능 부분에서는 비교적 뛰어난 스펙을 가지고 있다.
  • Maven에서 사용하는 XML로 빌드를 정의하기에는 어려움이 많다. 우선 가독성이  떨어지고, 의존관계가 복잡한 프로젝트를 설정하기에는 부적절하다.
  • Gradle은 groovy를 사용하기 때문에, 동적인 빌드는 Groovy 스크립트로 플러그인을 호출하거나 직접 코드를 짜면 된다. 
  • Configuration Injection 방식을 사용해 Maven에서 공통 모듈을 상속해서 사용하는 단점을 커버했다. 
  • 설정 주입 시 프로젝트의 조건을 체크할 수 있어서 프로젝트 별로 주입되는 설정을 다르게 할 수 있다.
  • 설정 파일만 보더라도 Gradle이 직관적으로 보기 편하다.

 

그리고 Gradle이 Maven보다 최대 100배 정도 빠르다.

728x90
728x90

🛠Controller

  • 마지막으로 Api 컨트롤러를 구현할 것이다.
  • RestController를 사용할 것이다.
    • 전통적인 Spring MVC의 컨트롤러인 @Controller는 주로 View를 반환하기 위해 사용한다.
    • @RestController는 Spring MVC Controlle에 @ResponseBody가 추가된 것이다. 당연하게도 RestController의 주용도는 Json 형태로 객체 데이터를 반환하는 것이다.

 

➡️SaveCategory

 

@PostMapping
public ResponseEntity<Void> saveCategory(@Valid@RequestBody SaveRequest request){
    categoryService.createCategory(request);
    return OK;
}

 

우선 부모 카테고리를 넣으면 정상적으로 값이 들어간다.

이후 자식 카테고리를 넣으면 정상적으로 부모 밑으로 값이 들어가는 것을 확인할 수 있다.

 

 

➡️getCategoeyById

@GetMapping("/{id}")
 public ResponseEntity<CategoryResponse> getCategoryById(@PathVariable(name = "id") Long categoryId){
    return categoryService.getCategoryById(categoryId);
}

 

이제 조회를 해보자 . 위에서 만든 카테고리를 조회해보자. (부모와 자식을 모두 조회할 것이다.)

부모를 조회하면 자식까지 정상적으로 조회되는 것을 볼 수 있다.

 

➡️updateCateogory

@PatchMapping("/{id}")
 public ResponseEntity<Void> updateCategory(@PathVariable(name = "id") Long id,
                               @Valid@RequestBody SaveRequest request){
    categoryService.updateCategory(id, request);
}

 

이제 수정을 해볼 것이다.

 

정상적으로 수정이 되었다. 

 

➡️deleteCategory

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCategory(@PathVariable(name = "id")Long id){
    categoryService.deleteCategory(id);
}

 

이제 삭제를 해보자

삭제는 자식 카테고리가 있으면 삭제가 불가능하도록 로직이 구현되어 있다.

만약 자식 카테고리가 있다면 예외가 정상적으로 던져진다.

 

이제 정상적인 삭제를 해보자

요청을 하면 쿼리가 나가는지 보자

상적으로 쿼리가 나가고 DB에서 값이 삭제되었다.

 

 

 


이로써 카테고리 로직 구현은 마무리가 되었다. 사실 구현하기 전에는 어려움이 없을 것이라고 생각했는데 생각보다 신경 써야 하는 부분이 많았다. 이런 로직을 자주 접하면서 더 최적화할 수 있도록 해야겠다.

 

😸예제코드

https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex: 📁블로그 예제 코드

📁블로그 예제 코드 . Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com

 

728x90
728x90

 

이번 포스팅은 Service를 구현하고 그에 대한 테스트 코드를 작성할 것이다.

 

우선 Service를 구현하기 전에 계층 간 데이터 전송을 하기 위해 먼저 DTO를 구현한다.

 

🧷DTO

@Getter
@NoArgsConstructor
public static class SaveRequest{
   private String categoryName;
   private String parentCategory;
   private Map<String,CategoryInfo> subCategory;

   @Builder
   public SaveRequest(String categoryName, String parentCategory, Map<String, CategoryInfo> subCategory) {
       this.categoryName = categoryName;
       this.parentCategory = parentCategory;
       this.subCategory = subCategory;
     }
   public Category toEntity(){
          Category category = Category.builder()
                    .categoryName(categoryName)
                    .build();
            return category;
        }
 }
  • childCategory를 get 메서드를 통해 json형식으로 데이터를 넘길 때 , Map으로 넘겨준다.

🗞Service

createCategory()

@Transactional
public void createCategory(SaveRequest request){
   Boolean existByName = categoryRepository.existsByCategoryName(request.getCategoryName());
   if (existByName){
        throw new IllegalArgumentException("카테고리 중복");
   }

   if (request.getParentCategory() == null){
       SaveRequest rootCategory = SaveRequest.builder()
                .categoryName(request.getCategoryName())
                .parentCategory(null)
                .build();

         categoryRepository.save(rootCategory.toEntity());
   }else{
         String parentCategory1 = request.getParentCategory();
         Category parentCategory = categoryRepository.findByCategoryName(parentCategory1)
                 .orElseThrow();
         Category category = Category.builder()
                 .categoryName(request.getCategoryName())
                 .parentCategory(parentCategory)
                 .build();
        parentCategory.getChildCategory().add(request.toEntity());
        categoryRepository.save(category);
      }
 }
  • 우선 첫번째로 카테고리 이름이 중복되는지 검증을 해준다. 만약 같은 카테고리 이름이 존재한다면 예외를 던져준다.
  • 만약 부모 카테고리가 없는 경우는 null값을 넣어준다.(추후에 다른 Default값이 들어가게 구현 예정)
  • 부모 카테고리가 있다면 검색을 한 후 자식 카테고리에 현재 카테고리를 넣어준다.

 

다음은 조회 로직이다.

 

🗓GET

@Getter
@NoArgsConstructor
@Builder
public static class CategoryResponse{
    private Long id;
    private String categoryName;
    private Map<String,CategoryInfo> subCategory=new HashMap<>();

    public CategoryResponse(Long id, String categoryName, Map<String, CategoryInfo> subCategory) {
         this.id = id;
         this.categoryName = categoryName;
         this.subCategory = subCategory;
     }
 }

조회할 데이터를 받아올 DTO를 생성한다.

public CategoryDto.CategoryResponse toCategoryResponse(){
        return CategoryDto.CategoryResponse.builder()
                .id(this.id)
                .categoryName(this.categoryName)
                .subCategory(
                    this.childCategory.stream().collect(Collectors.toMap(
                            Category::getCategoryName, CategoryDto.CategoryInfo::new
                    ))
             )
            .build();
}

다음 Entity클래스에 조회시 받아올 데이터에 대한 값을 스펙을 설정해준다.

@Transactional(readOnly = true)
    public CategoryResponse getCategoryById(Long id){
        CategoryResponse response = categoryRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("찾는 카테고리가 존재하지 않음"))
                .toCategoryResponse();
        return response;
}

서비스 계층에 메서드를 생성하고 아이디로 저장소에서 카테고리를 찾고 없으면 예외를 던져준다.

(추후 이름으로 조회하는 로직도 구현 예정)

 

다음으로 삭제와 수정 로직인데 이 로직들은 딱히 복잡한 것이 없다.

 

🗓Delete

@Transactional
public void deleteCategory(Long id){
     Category category = categoryRepository.findById(id)
            .orElseThrow(IllegalArgumentException::new);
    categoryRepository.deleteById(id);
}
  • Id로 카테고리를 검색하고 만약 카테고리가 존재하지 않으면 예외를 던진다.
  • 그리고 자식 카테고리가 존재하는 경우 삭제할 수 없도록 예외를 던져준다.

 

🗓Update

 public void update(SaveRequest request){
        this.categoryName = request.getCategoryName();
        this.parentCategory = request.toEntity().getParentCategory();
    }

entity 클래스에 update 메서드를 생성한다.

 

@Transactional
public void updateCategory(Long id,SaveRequest request){
    Category category = categoryRepository.findById(id).orElseThrow(IllegalArgumentException::new);

     category.update(request);
}

우선 수정 로직은 저장과 비슷하지만 categoryRepository에서 id로 카테고리를 찾은 후 업데이트해준다.

 

 

🗓TestCode

  • 진짜 테스트 코드는 서비스가 커지고 로직이 복잡할수록 중요하다. 
  • 내가 개발했지만 서비스가 커지면 뭐가 뭔지 헷갈릴 때가 있다. 그러므로 핵심 로직을 통과하기 위한 데이터를 생성하여 비즈니스 로직을 디버깅하고, 문제를 발견하는 작업이 중요하다.

 

이제 Service에 대한 테스트 코드를 작성해보자

 

우선 기본 설정과 들어갈 데이터 값을 설정해준다.

@ExtendWith(MockitoExtension.class)
class CategoryServiceTest {

    @InjectMocks
    CategoryService categoryService;
    @Mock
    CategoryRepository categoryRepository;



    public CategoryDto.SaveRequest request1(){
        return CategoryDto.SaveRequest.builder()
                .categoryName("category1")
                .parentCategory(null)

                .build();
    }
}
  • @RunWith(MockitoExtension.class) : JUnit5부터 @ExtendWith(MockitoExtension.class)를 사용한다.
  • @InjectMocks : @Mock이 붙은 목객체를 @InjectMocks이 붙은 객체에 주입시킬 수 있다.
    @Mock : mock 객체를 생성해준다.
  • assertThat으로 값을 비교해준다.

 

⛏save_category()

@Test
@DisplayName("카테고리 생성")
void save_category()throws Exception{
    //given
    CategoryDto.SaveRequest request1 = request1();

    //when
    when(categoryRepository.existsByCategoryName(request1.getCategoryName()))
            .thenReturn(false);
    categoryService.createCategory(request1);

    //then
    Assertions.assertThat(request1.getCategoryName()).isEqualTo(request1().getCategoryName());
    Assertions.assertThat(request1.getParentCategory()).isEqualTo(null);

}

 

update_category()

@Test
@DisplayName("카테고리 수정")
void update_category()throws Exception{
    //given    
    Category category = request1().toEntity();
    Long id = category.getId();

    //when
    when(categoryRepository.findById(id)).thenReturn(Optional.of(category));
    categoryService.updateCategory(id,request2());

    //then
    Assertions.assertThat(category.getCategoryName()).isEqualTo(request2().getCategoryName());
}

 

delete_category()

@Test
@DisplayName("카테고리 삭제")
void delete_category()throws Exception{
    //given
    Category category = request1().toEntity();
    Long id = category.getId();

    //when
    when(categoryRepository.findById(id)).thenReturn(Optional.of(category));
    categoryService.deleteCategory(id);

    //then
    verify(categoryRepository,atLeastOnce()).deleteById(id);

}

 

 

테스트 코드를 실행하면 정상적으로 통과된다.

 

 


 

😸예제코드

https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex: 📁블로그 예제 코드

📁블로그 예제 코드 . Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com

 

 

 

728x90
728x90

 

토이 프로젝트를 진행하면서 카테고리를 만드는데 무한 뎁스로 만드는 과정을 기록하려 한다.

Stack : Java,Gradle, Spring data jpa ,Mysql

위 사진 처럼 유연하게 설계하여 무한하게 카테고리가 생성이 가능한 로직을 구현할것이다.

 

🔓요구조건 

  1. Entity 구현
    • 하나의 테이블에서 구현해야하기 떄문에 Self Join 사용하여 구현 
  2. Repository 인터페이스 구현
    • Spring data JPA를 사용을 위해 JpaRepository를 상속 받는다.
  3. DTO 구현
    • Entity는 DB를 생성하고 DB를 가지고 계층간의 소통을 하기 위해 DTO를 구현할 것이다.
    • Entity에서 list로 담은 childCategory는 DTO에서는 Map으로 구현한다.
  4. Service 구현 
    • Save : 카테고리를 생성하는 로직이다.( 카테고리 이름이 중복되는지 검증 후 생성,부모 자식 카테고리를 가지고 있는지 검증 )
    • delete :  카테고리를 삭제하는 로직이다. ( 자식 카테고리가 존재하면 삭제할 수 없다.)
    • update : 카테고리를 업데이트하는 로직이다.
    • get : 카테고리를 Id와 name으로 조회하는 로직이다. (자식 카테고리 존재시 같이 조회된다.)
  5. Service Test Code 구현 
    • 테스트 코드는 항상 중요하다.
    • 서비스 단위 테스트 코드이다.
  6. Controller 구현
    • Api 통신을 위해 반환 로직을 구현

 

 

1. Entity 구현 

  • 하나의 테이블에서 무한 뎁스를 구현해야하기 때문에 Self Join을 사용할 것이다.
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Category {

    @Id@GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "category_id")
    private Long id;

    private String categoryName;

    @ManyToOne(fetch = FetchType.LAZY)
    private Category parentCategory;

    @OneToMany(mappedBy = "parentCategory")
    private List<Category>childCategory =new ArrayList<>();

 

    @Builder
    public Category(String categoryName, Category parentCategory, List<Category> childCategory) {
        this.categoryName = categoryName;
        this.parentCategory = parentCategory;
        this.childCategory = childCategory;

    }
}
  • 여기서 주의깊게 볼것은 parentCategory childCategory이다. 
  • 서로 물고 무는 무한뎁스 구조를 위해 두 칼럼끼리 연관관계를 맺고 mappedBy속성을 통해서 parentCategory를 연관 관계 주인으로 설정해준다.(주인을 설정해주지 않으면 조회시 계속 서로를 참조하는 무한루프가 발생한다!)
  • JPA에서 연관관계를 설정할때 ManyToOne,OneToOne은 fetchType.EAGER이 default이다. 그래서 지연 참조(detchType.Lazy)를 사용해줘야 N+1 이슈 등 문제가 생기지 않는다.

2.Repository

public interface CategoryRepository extends JpaRepository<Category,Long> {
    Optional<Category>findByCategoryName(String name);
    Boolean existsByCategoryName(String name);
}
  • extends JpaRepository는 Spring data jpa를 사용하기 위해 무조건 상속 받아야한다.
  • Spring data jpa는 메서드 이름명으로 검색 조건을 만들수 있다.(find + By + Entity 필드값)
  • existsByCateoryName은 카테고리 이름이 중복되는지 검증하기 위해 Boolean 타입으로 리턴 받는다.

 

다음 포스팅은 서비스와 서비스에 대한 테스트 코드를 포스팅할 예정이다.

 

😸예제 코드

https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex: 📁블로그 예제 코드

📁블로그 예제 코드 . Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com

 

728x90
728x90

프로젝트 진행 중 컨트롤러의 메서드가 로그인 여부를 확인하는 부가기능을 동시에 처리하는 경우가 존재한다.

이런식으로 반복되는 로직을 AOP,Interceptor,Filter 등을 통해 반복되는 부가기능을 공통적으로 처리할 수 있다.

 

로그인 확인이 필요한 경우

프로젝트를 진행하다가 보면 로그인 없이 접근 가능한 페이지가 있고 로그인을 한 경우에만 접근이 가능한 페이지가 존재한다.

예를들어 마이페이지,비밀번호 변경,상품 등록하기 등이 있다.

 

Interceptor를 적용하기 전 로그인 확인 과정

  • 세션에서 현재 로그인된 사용자의 정보를 꺼내온다.
  • 세션에서 꺼낸 값이 Null이라면 , 해당 사용자는 로그인을 하지 않은 상태이므로 401 UNAUTHORIZED를 반환한다.
  • 만약 정상적으로 session에서 로그인 정보를 꺼낼수 있다면 200 OK 를 반환한다.

controller

1
2
3
4
5
6
7
@GetMapping("/my-info")
    public ResponseEntity<UserInfoDto> myInfo() {
        String currentUser = sessionloginService.getLoginUser();
        UserInfoDto userInfoDto = userService.getUserInfo(currentUser);
        return ResponseEntity.ok(loginUser);
    }
 
cs

 

service

1
2
3
4
5
6
7
public String getLoginUser() {
    String userId = session.getAttribute(USER_ID);
    if(userId == null) {
        throw new UnauthenticatedUserException();
    }
   return userId; 
}
cs

이제 이 과정을 Interceptor를 적용해서 해당 메소드가 자신의 핵심 기능만 집중할 수 있도록 리펙토링 해볼것이다.

 

어노테이션 생성 

 

1
2
3
4
@Retention(RUNTIME)
@Target(METHOD)
public @interface LoginCheck {
}
cs
  • @LoginCheck : 현재 사용자의 로그인 여부를 확인한다.
  • @Retention: 메모리를 어느 시점까지 가져갈지 여부 설정
  • @Target : 해당 어노테이션이 어느 위치까지 사용될지 지정한다.

 

Interceptor 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
@RequiredArgsConstructor
public class LoginCheckInterceptor implements HandlerInterceptor {
 
    private final SessionLoginService loginService;
 
 
 
    //컨트롤러 메서드 실행되기전
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
 
       
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            LoginCheck loginCheck = handlerMethod.getMethodAnnotation(LoginCheck.class);
 
 
            if (loginCheck == null) {
                return true;
            }
 
 
            if (loginService.getLoginUser() == null) {
                throw new UnauthenticatedUserException("로그인 후 이용 가능합니다.");
            }
 
        }
    return true;
 
}
cs

Interceptor는 preHandler() , postHandler() , afterCompletion() 로 구성되어 있다.

  • preHandler() : 컨트롤러 메서드 실행되기전에 실행된다.
  • postHandler() : 컨트롤러 메서드 실행 직후, view가 렌더링 되기전 실행됨
  • afterCompletion() : view 페이지 렌더링 후 실행 

Interceptor는 메서드 실행 직후에 해당 요청을 가로채서 요청을 한 사용자의 로그인 여부를 판단한다.

loginCheck는 해당 Handler에 LoginCheck 어노테이션이 존재하는지 확인한다.

  • loginCheck가 null이라면 로그인 없이 접근 가능한 페이지 이므로 true를 반환한다.
  • loginCheck가 null이 아니라면 session에서 로그인 정보를 꺼내서 null 여부를 판단한다. null이라면 로그인 후 이용 가능하다는 Exception을 날린다.
  • 모든 검증을 통과했다면 로그인이 완료된 상태로 true를 리턴해 다음 작업을 실행한다.

이제 Interceptor를 등록한다.

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
 
    private final LoginCheckInterceptor loginCheckInterceptor;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor);
    }
 
}
cs

 

WebMvcConfigurer는 스프링 부트가 기본적으로 설정한 MVC설정에 추가적으로 기능을 커스터마이징 할 수 있다.(스프링 부트 개발환경에서만 가능)

1
2
3
4
5
6
7
@LoginCheck
@GetMapping("/my-infos")
public ResponseEntity<UserInfoDto> myPage() {
   String currentUser = loginService.getLoginUser();
   UserInfoDto loginUser = userService.getUserInfo(currentUser);
   return ResponseEntity.ok(loginUser);
}
cs

이제 유저의 로그인 여부를 확인하고 사용자의 ID를 가져오는 Service에서 불필요한 예외처리를 할 필요가 없다.

 

728x90

'Dev > Spring' 카테고리의 다른 글

[Spring] JPA+Spring으로 카테고리 로직 구현 - 2  (0) 2021.12.10
[Spring] JPA+Spring으로 카테고리 로직 구현 - 1  (0) 2021.12.10
[Spring] AOP(Aspect Oriented Programming)  (0) 2021.09.10
ResponseEntity  (0) 2021.08.25
@Transactional  (1) 2021.08.24
728x90

📖 AOP(Aspect Oriented Programming)

개발을 하다보면 공통적인 기능이 많이 발생한다. 이러한 공통 기능을 모든 모듈에 적용하기 위해 상속을 하지만 자바에서는 다중상속이 불가능해서 상속을 받아 공통 기능을 부여하기에는 한계가 있다.

AOP를 사용하지 않는다면 중복 코드가 발생할 소지가 있고, 코드의 변경이 필요하면 여러코드에 종속적으로 변경이 필요하고 핵심 비즈니스 로직에 호출 시간 측정이라는 부수적인 로직이 추가되어 가독성과 효율성이 떨어지는 문제가 발생한다.

이러한 문제점을 해결하기 이해서는 AOP가 필요하다.AOP는 새로운 프로그래밍 패러다임이 아니라 객체 지향 프로그래밍(OOP)를 돕는 보조적인 기술로, 핵심적인 관심사항과 공통 관심사항으로 분리 시키고 각각을 모듈화 하는 것이다.

 

📖AOP(Aspect Oriented Programming)의 주요 개념

  • Aspect : 흩어진 관심사를 모듈화 한 것이다. 주로 부가기능을 모듈화한다.
  • Target : Aspect를 적용하는곳 
  • Advice : 실질적인 부가기능을 담은 구현체
  • JoinPoint : Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점,필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능
  • PointCut : JoinPoint의 상세한 스팩을 정의한 것.

 

📖AOP(Aspect Oriented Programming)의 장점 

  • 공통 관심 사항을 핵심 관심사항으로부터 분리시켜 핵심 로직을 깔끔하게 유지할 수 있다.
  • 가독성과 유지보수성을 높일 수 있다.
  • 각각의 모듈에 수정이 필요하면 다른 모듈의 수정 없이 해당 로직만 변경하면 된다.
  • 공통 로직을 적용할 대상을 선택할 수 있다.

📖AOP(Aspect Oriented Programming)특징

 

  • 프록시 패턴 기반의 AOP 구현체, 프록시 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위해서이다.
  • 스프링 빈에만 AOP를 적용 가능하다.
  • 모든 AOP 기능을 제공하는 것이 아닌 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제에 대한 해결책을 지원하는 것이 목적



📖AOP 기본적인 실습

특정 어노테이션이 붙은 포인트에 해당 Aspect를 실행할 수 있는 기능도 제공한다. 

 

@Around 어노테이션은 타겟 메서드를 감싸서 특정 Advice를 실행한다는 의미이다. 

@Aspect 어노테이션을 붙여 이 클래스가 Aspect를 나타내는 클래스라는 것을 명시하고 @Component를 붙여 스프링 빈으로 등록한다.

 

@Target은 어노테이션이 적용될 레벨을 의미한다. 

 

@Retention 어노테이션으로 어느 시점까지 어노테이션의 메모리를 가져갈 지 설정하고, @Target 어노테이션으로 필드, 메소드, 클래스, 파라미터 등 선언할 수 있는 타입을 설정하면 대부분 커스텀 어노테이션은 쉽게 생성할 수 있다. 

 

결과 @ExecutionTimeChecker 를 붙인 메서드에 Aspect가 추가된것을 알 수 있다.

 

😸GITHUB 소스코드 : https://github.com/ryudongjae/blog-ex

 

GitHub - ryudongjae/blog-ex

Contribute to ryudongjae/blog-ex development by creating an account on GitHub.

github.com


 

REFERENCE

728x90

'Dev > Spring' 카테고리의 다른 글

[Spring] JPA+Spring으로 카테고리 로직 구현 - 1  (0) 2021.12.10
[Spring] 어노테이션으로 로그인 여부 확인(Interceptor)  (0) 2021.09.21
ResponseEntity  (0) 2021.08.25
@Transactional  (1) 2021.08.24
Spring REST Docs  (0) 2021.08.23

ResponseEntity

ryudjae
|2021. 8. 25. 02:29
728x90

➡️ResponseEntity란?

SpringFrameword에서 제공하는 클래스 중 HttpEntity라는 클래스가 존재한다. 이것은 HTTP 요청(request) 또는 응답(response)에 해당하는 HttpHeader와 HttpBody를 포함하는 클래스이다.

1
2
3
4
5
6
7
public class HttpEntity<T> {
 
    private final HttpHeaders headers;
 
    @Nullable
    private final T body;
}
cs

 

1
2
3
public class RequestEntity<T> extends HttpEntity<T>
 
public class ResponseEntity<T> extends HttpEntity<T>
cs

ResponseEntity와 RequestEntity는 HttpEntity를 상속 받는다. 따라서 RepsonseEntity는 사용자의 HttpRequest에 대한 Response데이터를 포함하는 클래스이다. 그러므로 HttpStaus,HttpHeader,HttpBody를 포함한다.

ResponseEntity의 생성자를 보면 this()를 통해서 매게변수가 3개인 생성자를 호출해 결국인 아래에 보이는 매게변수가 3개인 성성자로 가게된다.

1
2
3
4
5
6
7
8
public ResponseEntity(HttpStatus status) {
    this(nullnull, status);
}
 
public ResponseEntity(@Nullable T body, HttpStatus status) {
    this(body, null, status);
}
 
cs

 

지금부터 간단한 예제로 보자

 

1
2
3
4
5
6
7
8
9
10
@RestController
public class BasicController {
 
    @GetMapping("/test")
    public ResponseEntity getUser(){
        return new ResponseEntity(HttpStatus.OK);
    }
}
 
 
cs

위처럼 컨트롤러를 작성한 뒤 포스트맨으로 요청을 해보면 상태코드가 200으로 나온다.

 

그리고 상태코드(status),헤더(header) ,응답데이터(ResponseData)를 담는 생성자도 존재한다.

1
2
3
4
5
6
7
8
9
10
public class ResponseEntity<T> extends HttpEntity<T> {
    
    public ResponseEntity(@Nullable T body, 
@Nullable MultiValueMap<StringString> headers,
  HttpStatus status) {
        super(body, headers);
        Assert.notNull(status, "HttpStatus must not be null");
        this.status = status;
    }
}
cs
     

위 생성자처럼 ResponseEntity를 이용해서 클라이언트에게 응답을 보내는 예제를 한번 해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
public class Message {
    private Status status;
    private String message;
    private Object data;
 
    public Message() {
        this.status = Status.BAD_REQUEST;
        this.data = null;
        this.message = null;
    }
}
 
cs

상태코드,메시지,데이터를 담을 필드를 추가했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
public enum Status {
 
    OK(200"OK"),
    BAD_REQUEST(400"BAD_REQUEST"),
    NOT_FOUND(404"NOT_FOUND"),
    INTERNAL_SERER_ERROR(500"INTERNAL_SERVER_ERROR");
 
    int statusCode;
    String code;
 
    Status(int statusCode, String code) {
        this.statusCode = statusCode;
        this.code = code;
    }
}
 
cs

상태코드를 몇가지만 enum클래스로 만들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class BasicController {
 
    @GetMapping("/test/{id}")
    public ResponseEntity<Message> getUser(@PathVariable Long id){
        User user = User.builder()
                .email("r213213@Test.com")
                .nickname("aa")
                .phone("00011111111")
                .build();
        Message message = new Message();
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(new MediaType("application","json", Charset.forName("UTF-8")));
 
        message.setStatus(Status.OK);
        message.setMessage("성공ㅋ");
        message.setData(user);
        return new ResponseEntity<>(message,httpHeaders,HttpStatus.OK);
    }
}
 
cs

그리고 위와 같이 Controller를 하나 만든 후에, id를 통해서 User를 가져오고 Message 클래스를 통해서 StatusCode, ResponseMessage, ResponseData를 담아서 클라이언트에게 응답을 보내는 코드이다.

아래는 컨트롤러를 작성한대로 포스트맨으로 실행해본 결과이다.

data부분은 내가 따로 만든 User클래스이고, 값도 다 안넣어서 null이 나온다. 그래서 컨트롤러에서 따로 넣은 부분만 확인이 가능하다.이런식으로 상태,메시지,데이터가 잘 응답하는것을 볼 수 있다.

 

일단 이렇게 ResponseEntity에서 기본적인 내용만 알아봤는데 이 내용을 이해할려면 RestApi를 공부를 조금이라도 한 상태에서 보면 더 이해가 잘될것이다.

아직 저도 RestApi에 대해 공부하는 중이라 잘못된 내용이 있을수도 있습니다.과감하게 피드백 주시면 감사하겠습니다.

 

 


REFERENCE

RestAPI

728x90

'Dev > Spring' 카테고리의 다른 글

[Spring] 어노테이션으로 로그인 여부 확인(Interceptor)  (0) 2021.09.21
[Spring] AOP(Aspect Oriented Programming)  (0) 2021.09.10
@Transactional  (1) 2021.08.24
Spring REST Docs  (0) 2021.08.23
DAO,DTO,Entity Class  (0) 2021.08.13