1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

회원 가입을 진행할 때, Email 중복 검사하는 로직을 추가했습니다.

 

처음에는 Email로 조회해서 리스트의 갯수가 0일 경우 통과하는 로직으로 작성했습니다.

그 다음으로는 JPA 강의를 들으면서 정리한 코드를 살펴보았는데,

Exceopion이 왜 거기서 나와...?

 

public void validateDuplicateMember(Member member) {
    List<Member> findMembers = memberRepository.findByName(member.getName());
    if(!findMembers.isEmpty()){
        throw new IllegalStateException("이미 존재하는 회원입니다.");
    }
}

 

그대로 코드를 사용할 수는 있지만, IllegalStateException 대신 CustomException을 사용해보고 싶었습니다.

왜냐면 HttpStatus도 함께 사용하는 멋진 코드를 발견했기 때문이죠.

 

그렇기에 이 글을

  1. Error Code를 Enum 형태로 만들고
  2. CustomException을 만들어
  3. Test Code를 작성해 본다

를 정리한 글입니다.

 

 

1. Error code Enum 생성

package com.msgs.msgs.error;

import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public enum ErrorCode {

    // 회원
    CHECK_LOGIN_ID_OR_PASSWORD(HttpStatus.NOT_FOUND, "아이디 또는 비밀번호를 확인해주세요."),
    DUPLICATED_EMAIL(HttpStatus.BAD_REQUEST, "이미 존재하는 이메일 입니다.");

    private final HttpStatus httpStatus;
    private final String message;

    ErrorCode(HttpStatus httpStatus, String message) {
        this.httpStatus = httpStatus;
        this.message = message;
    }
}

 

제가 알던 Enum과는 모양이 달라서 많이 놀랐습니다.

 

Enum은 아래와 같이 간단한 열거형만 써봤기 때문이죠.

public enum ErrorCode {
    CHECK_LOGIN_ID_OR_PASSWORD,
    DUPLICATED_EMAIL,
}

 

Enum 요소에 특정 값을 연결하려면 필드값을 추가해주면 됩니다.

 

Enum과 필드값을 매핑해주면, if문의 사용을 줄여 가독성을 높일 수 있습니다.

 

이 때, 유의해야 할 점은 반드시 Enum 값 선언 이후에 이후에 필드/생성자가 위치해야 한다는 것입니다.

그리고 Enum class에서 자체적으로 name()를 제공하기 때문에, 필드값을 name을 사용하지 않는 것이 좋습니다.

 

+ Enum은 Enum 값으로 데이터를 찾을 수 있지만 필드의 경우에는 따로 메서드를 만들어 줘야 합니다.

// HttpStatus -> ErrorCode 조회
private static final Map<HttpStatus, ErrorCode> BY_HTTPSTATUS =
        Stream.of(values()).collect(Collectors.toMap(ErrorCode::getHttpStatus, e -> e));

public static Optional<ErrorCode> valueOfHttpStatus(HttpStatus httpStatus){
    return Optional.ofNullable(BY_HTTPSTATUS.get(httpStatus));
}

// Message -> ErrorCode 조회
private static final Map<String, ErrorCode> BY_MESSAGE =
        Stream.of(values()).collect(Collectors.toMap(ErrorCode::getMessage, e -> e));

public static Optional<ErrorCode> valueOfMessage(String message){
    return Optional.ofNullable(BY_MESSAGE.get(message));
}

 

 

2. CustomException 생성

ErrorCode를 받아주는 CustomException을 만들어줍니다.

@Getter
public class BusinessException extends RuntimeException{
    private ErrorCode errorCode;

    public BusinessException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }
}

 

RuntimeException도 상속을 받습니다.

 

3. Test Code 작성

1 기능, 1 테스트 코스..

특히 테스트 코드는 익숙하지 않기 때문에 기능에 맞춰 계속 추가해 볼 예정입니다.

 

@SpringBootTest
@Transactional
public class UserServiceTest {

	// ...

    @Test
    @DisplayName("이메일이 동일한 회원이 존재할 경우, 예외가 발생한다.")
    public void emailDuplicateCheck() throws Exception {
        // given
        User userA = new User();
        userA.setStatus("M");
        userA.setEmail("test@email.com");
        userA.setPhone("01023456789");
        userService.create(userA);

        String existingEmail = "test@email.com";

        // when

        // then
        BusinessException exception = assertThrows(BusinessException.class,
                () -> userService.emailDuplicateCheck(existingEmail));
        assertEquals(DUPLICATED_EMAIL, exception.getErrorCode());
    }
}

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

 

Java Enum 1편 : Enum 기본적인 사용

1. Overview Java Enum 타입은 일정 개수의 상수 값을 정의하고, 그 외의 값은 허용하지 않습니다. 과거에는 특정 상수값을 사용하기 위해선 모두 상수로 선언해서 사용했습니다. public static final String M

bcp0109.tistory.com

 

GitHub - dbswhd4932/shoppingmall_project: Java Springboot Jpa 를 이용한 쇼핑몰 API 프로젝트

Java Springboot Jpa 를 이용한 쇼핑몰 API 프로젝트. Contribute to dbswhd4932/shoppingmall_project development by creating an account on GitHub.

github.com

 

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

Test Code가 없던 1년 전 마실가실, 리팩토링을 하면서 Test Code가 생겼습니다.

예상 동작과 실제 동작을 비교하여 빠르고 정확한 테스트를 위해서 Test Code를 작성한다고 하는데,

개인적으로 가장 큰 장점은 독립적인 테스트가 가능하다는 것이라고 생각합니다.

 

저의 리팩토링은 Front가 아닌 Back이고, 따라서 바인딩은 수정이 안된 상황입니다.

화면에서는 정확하게 나오지 않을 확률이 99%입니다.

바인딩까지 수정하면 너무 긴 시간을 기다리게 됩니다.

그렇다고, System.out.println()을 하나하나 확인하기에는 정확성이 떨어질 수 있습니다.

 

또한, 다른 기능이 아직 리팩토링되지 않아 동작을 하지 않을 경우 새로 고친 코드가 맞는지는 이런 식으로 하다가는 프로그램이 완성되어야 알 수 있게 됩니다.

 

이에 대한 대안이 Test Code라고 생각합니다.

 

기존에 설정조차 안되어 있었기 때문에

  1. 환경 설정을 하고
  2. 테스트 코드를 작성해서
  3. 오류를 수정할 예정입니다.

 

1. 환경 설정

  • build.gradle
// 추가
dependencies {
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

// 추가
test {
	useJUnitPlatform()
}
// test 태스크가 정의될 때 설정을 직접 적용

// 삭제
task.named('test'){
	useJUnitPlatform()
}
// 동적으로 태스크를 참조하거나 태스크가 조건부로 생성되는 경우 사용

 

2. Test Code 작성

  • UserServiceTest.java
@SpringBootTest
@Transactional
public class UserServiceTest {
    @Autowired
    UserService userService;
    @Autowired
    UserRepository userRepository;

    @Test
    public void 회원가입() throws Exception {
        // given
        User user = new User();
        user.setStatus("M");
        user.setEmail("test@email.com");
        user.setPhone("010-2345-6789");

        // when
        Integer createdId = userService.create(user);

        // then
        assertThat(user).isEqualTo(userRepository.findOne(createdId));
    }
}
  • @SpringBootTest
    • 통합 테스트를 제공하는 기본적인 스프링 부트 테스트 어노테이션
  • NullPointerException
    • @Test import class에 유의
      • import org.junit.Test;
        → import org.junit.jupiter.api.Test;
    • @CreatedDate, @LastModifiedDate 사용 시, 
      • @EnableJpaAuditing, @EntityListeners(AuditingEntityListener.class) 추가

 

3. 오류 수정

사실 자잘한 오류는 문제가 아니었습니다.

테스트 자체가 안돌아갔던 큰 문제가 있었기 때문이죠.

 

contextLoads()

java.lang.IllegalStateException: Failed to load ApplicationContext for [WebMergedContextConfiguration@2caa5d7c testClass = com.msgs.main.MsgsApplicationTests, locations = [], classes = [com.msgs.main.MsgsApplication], contextInitializerClasses = [], activeProfiles = [], propertySourceLocations = [], propertySourceProperties = ["org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true"], contextCustomizers = [org.springframework.boot.test.autoconfigure.actuate.observability.ObservabilityContextCustomizerFactory$DisableObservabilityContextCustomizer@1f, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@3cc41abc, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@6f2cfcc2, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@ec2cc4, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2f4205be, org.springframework.boot.test.web.reactive.server.WebTestClientContextCustomizer@544820b7, org.springframework.boot.test.context.SpringBootTestAnnotation@d3394ad], resourceBasePath = "src/main/webapp", contextLoader = org.springframework.boot.test.context.SpringBootContextLoader, parent = null]
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:142)
	at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:127)
	at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:191)
	at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:130)
	at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:241)
	at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:138)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$10(ClassBasedTestDescriptor.java:377)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.executeAndMaskThrowable(ClassBasedTestDescriptor.java:382)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$11(ClassBasedTestDescriptor.java:377)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:310)
	at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:735)
	at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:734)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:762)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestInstancePostProcessors(ClassBasedTestDescriptor.java:376)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$instantiateAndPostProcessTestInstance$6(ClassBasedTestDescriptor.java:289)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:288)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$4(ClassBasedTestDescriptor.java:278)
	at java.base/java.util.Optional.orElseGet(Optional.java:364)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$5(ClassBasedTestDescriptor.java:277)
	at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:31)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:105)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:104)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:68)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$2(NodeTestTask.java:123)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:123)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:90)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at jdk.proxy1/jdk.proxy1.$Proxy2.stop(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [com.msgs.main.MsgsApplication]
	at app//org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:178)
	at app//org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:415)
	at app//org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:287)
	at app//org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:344)
	at app//org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:115)
	at app//org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:771)
	at app//org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:589)
	at app//org.springframework.boot.SpringApplication.refresh(SpringApplication.java:733)
	at app//org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:435)
	at app//org.springframework.boot.SpringApplication.run(SpringApplication.java:311)
	at app//org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$3(SpringBootContextLoader.java:137)
	at app//org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58)
	at app//org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46)
	at app//org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1405)
	at app//org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:545)
	at app//org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:137)
	at app//org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:108)
	at app//org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:184)
	at app//org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:118)
	... 87 more
Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'userController2' for bean class [com.msgs.user.controller.UserController2] conflicts with existing, non-compatible bean definition of same name and class [com.msgs.msgs.jwt.controller.UserController2]
	at app//org.springframework.context.annotation.ClassPathBeanDefinitionScanner.checkCandidate(ClassPathBeanDefinitionScanner.java:349)
	at app//org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:287)
	at app//org.springframework.context.annotation.ComponentScanAnnotationParser.parse(ComponentScanAnnotationParser.java:128)
	at app//org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:289)
	at app//org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:243)
	at app//org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:196)
	at app//org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:164)
	... 105 more

 

너무 긴 에러는 눈에 잘 안들어옵니다.

 

읽히는 에러만 찾아서 해결해봅니다.

전 보통 제일 긴 부분이나 Caused by를 먼저 봅니다.

Caused by: 
	org.springframework.context.annotation.ConflictingBeanDefinitionException: 
		Annotation-specified bean name 'userController2' for bean class [com.msgs.user.controller.UserController2] conflicts with existing, 
		non-compatible bean definition of same name and class [com.msgs.msgs.jwt.controller.UserController2]

 

찾았습니다🫠.

 

package가 다르면 Controller 이름이 같아도 되겠지라고 생각한 저의 실수입니다.

같은 이름의 Bean을 등록할 수 없습니다.

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

 

Spring Boot에서의 Test 어노테이션

Spring Boot는 기본적인 테스트 스타터를 제공한다. 스타터에 웬만한 테스트 라이브러리들을 한데 뭉쳐놓았기 때문에 편리하게 사용할 수 있다. 스타터는 크게 두 가지 모듈로 구성된다.

velog.io

 

 

OrderServiceTest 코드의 NullPointerExce... - 인프런 | 커뮤니티 질문&답변

누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.

www.inflearn.com

 

[Spring] Spring Data JPA에서 Auditing 사용하는 법

JPA AuditingEntityListener 알아보기 위의 코드는 제가 작성했던 코드인데요. 누군가 저에게 아래와 같이 물었습니다. LocalDateTime.now() 코드는 왜 작성한거야? 위의 질문을 듣고 아.. BaseEntity를 통해서 생

devlog-wjdrbs96.tistory.com

 

 

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

RESTful한 구현이 안된 프로젝트는 1년 전 마실가실입니다.

 

1년 후 마실가실은 RESTful 할 수 있습니다.

 

간단한 정의는

  자원의 이름(표현)으로 자원의 상태(정보)를 주고 받는 것

입니다.

 

자원은 URI로, 자원의 행위는 HTTP Method로 표현 → 행위에 맞는 적절한 HTTP method를 사용한 것을 RESTful하다고 합니다.

  • 조회: GET
  • 생성: POST
  • 일부 수정: PATCH / 전체 수정: PUT
  • 삭제: DELETE

REST API 설계 규칙

  1. 소문자 사용
  2. _ 대신 - 사용
  3. 마지막에 슬래시(/)를 포함하지 않음
  4. 행위를 포함하지 않음
    행위는 URI 대신 Http Method를 사용하여 전달
  5. 파일 확장자는 URL에 포함시키지 않음
  6. 명사 사용, 예외적으로 컨트롤 자원을 의미하는 경우 동사 사용
  7. URI에 작성되는 영어를 복수형으로 작성

 

// 기존
@RestController
@RequestMapping("user")
public class UserController {
    @PostMapping("/signup")
    public void userSignUp(@RequestBody User user) {
        userService.signUp(user);
    }
}

// 리팩토링
@RestController
@RequestMapping("api/v2/users")
@RequiredArgsConstructor
public class UserController {
    @PostMapping("/new")
    public String create(@RequestBody User user){
        System.out.println("UserController.create");

        Integer id = userService.create(user);
        return id.toString();
    }
}
  • signup → new
  • user → users

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

 

REST API URL 규칙

API 개발을 하는데 있어서 URI를 어떻게 명명할지에 대한 정리를 하기 위해 포스팅 하였다. 1. RESTful API 란 Rest : Representational State Tranfer의 약자로 웹을 이용할때 제약조건들을 정의하는 소프트웨어

dev-cool.tistory.com

 

[간단정리] REST, REST API, RESTful 특징

개요 REST, REST API, RESTful 특징 알아보기 REST REST 정의 REST(Representational State Transfer)는 월드 와이드 웹과 같은 분산 하이퍼미디어 시스템을 위한 소프트웨어 아키텍처의 한 형식 REST는 기본적으로 웹

hahahoho5915.tistory.com

 

REST란? REST API 와 RESTful API의 차이점

참고 REST(REpresentational State Transfer)란? REST의 정의 "REpresentational State Transfer" 의 약자로, 자원을 이름(자원의 표현)으로 구분해 해당 자원의 상태(정보)를 주고 받는 모든 것을 의미합니다. 즉, 자원(

dev-coco.tistory.com

 

 

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

Table을 만드는 방법은 크게 2가지입니다.

* Database에서 직접 CREATE TABLE 하기

* JPA의 DDL 생성 기능 활용하기

 

원래도 JPA의 DDL문 생성 기능을 활용했으므로 Column만 먼저 바꿔볼 예정입니다.

관계에 의한 Mapping도 변경을 해줘야하는데, 그건.. 차근차근 해보겠습니다🫠.

 

+ 리팩토링

  • UserEntity.java → User.java
    • 의미없는 Annotation 정리(사용하지 않는 값, Default값 사용 등)
    • PK값 데이터 타입을 조회 성능을 고려하여 varchar → Integer 변경 
    • 데이터 입력 자동화
      • @CreatedDate, @LastModifiedDate

 

+ 리팩토링

  • TripSchedule.java → Trip.java
  • 의미없는 Annotation 정리
    • @Buider와 @NoArgsConstructor
      • 상황
        • Trip Entity는 기본 생성자가 필요한 상황이나 모든 필드를 대상으로 하는 AllArgsConstructor는 필요 없는 상황
      • 문제
        • @Buider, @NoArgsConstructor만 사용 시, 'constructor Trip in class Trip cannot be applied to given types' 오류 발생
      • 원인
        • 클래스 레벨 @Builder
          • 생성자가 없는 경우, @Builder는 클래스의 모든 멤버 변수를 파라미터로 받는 생성자를 생성
          • 생성자가 이미 있는 경우, @Builder는 기존의 생성자를 활용하며 새로운 생성자를 생성하지 않음
        • @NoArgsConstructor에 의해 기본 생성자 생성 → @Builder 추가적인 생성자 생성 X
        • 이후, @Builder에 의해 생성된 빌더 클래스 내부에서 해당 타입의 인스턴스를 생성할 때 모든 멤버 변수를 초기화하는 생성자 호출 시도
          • required: no arguments
          • found: all arguments
      • 해결 방법
        • @Builder 어노테이션을 생성자 레벨에서 사용
        • @AllArgsConstructor 추가 사용

 

+ 리팩토링

  • TripDailySchedule.java → TripDay.java
    • ERD에 맞춰서 수정

 

+ 리팩토링

  • TripDatailSchedule.java → DayDestination.java
    • ERD에 맞춰서 수정

 

+ 리팩토링

(2024.08.04)

  1. 복합키 설정
    • 일정 테이블과 리뷰 사진 테이블에 복합키 설정 추가
    • @IdClass 사용
    • 연관 관계 매핑 시, @JoinColumns로 변경
  2. 양방향 연관 관계 매핑 → 단방향 연관 관계 매핑
    • 양방향 참조 관계 설정으로 인한 toString()의 무한 루프 오류를 줄이기 위해 초기 단방향 참조 관계 설정 후, 필요시 양방향 참조 관계로 변경 예정
    • 양방향 연관 관계 설정 시 유의 사항
      • 주인 = 외래키를 보유하고 있는 곳, 1:N 관계에서 N에 해당하는 곳, @ManyToOne 어노테이션을 사용하는 곳

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

 

[JPA_Basic] ERD를 기반으로 한 Entity 작성

이 글은 김영한의 [자바 ORM 표준 JPA 프로그래밍 - 기본편]을 수강하며 정리한 글입니다. 👉 기본 환경 - Language: Java - DB: H2 Database - IDE: IntelliJ ERD Member Entity 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 2

hj0216.tistory.com

 

JPA @CreatedDate @LastModifiedDate 생성 시간, 수정 시간이 저장되는 원리

@CreatedDate, @LastModifiedDate 데이터를 저장할 때 '생성된 시간 정보'와 '수정된 시간 정보'는 여러모로 많이 사용되고 또 중요합니다. JPA를 사용하면서 @CreatedDate, @LastModifiedDate를 사용하여 생성된 시

wildeveloperetrain.tistory.com

 

Lombok - @Builder와 @NoArgsConstructor 함께 사용시 발생하는 오류와 해결법

Lombok 라이브러리는 Java에서 반복적인 코드를 줄이기 위한 유용한 도구 중 하나이다.그 중에서도 @Builder와 @NoArgsConstructor는 자주 사용되는 어노테이션들이다.그러나 두 어노테이션을 함께 사용하

velog.io

 

[JPA 프로그래밍] 복합키와 비식별&식별 관계 매핑

해당 글은 김영한 님의 "자바 ORM 표준 JPA 프로그래밍" 을 스터디 하면서 정리하는 글 입니다 !👨‍💻

velog.io

 

[JPA] 식별 관계 & 복합 키

DB에서 두 테이블간에 관계를 나타낼때는 총 2가지로 분류할 수 있다 1. 상대방의 PK를 자신의 PK이자 FK로 사용 = 식별관계 2. 상대방의 PK를 자신의 FK로 사용 = 비식별관계 필수적 비식별 관계(Mandat

cs-ssupport.tistory.com

 

[JPA] 단방향 연관관계와 양방향 연관관계 - 엔티티 매핑(Entity Mapping) - 5

본 내용은 온라인 강의 사이트 인프런의 김영한 님의 강의 내용이 포함되어 있습니다. '자바 ORM 표준 JPA 프로그래밍 - 기본편' 자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의 JPA를 처음 접

ittrue.tistory.com

 

 

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

기존에는 네이버 클라우드에 DB 연결되어있었는데, 기간 만료의 문제로 Local로 변경하여 연결을 해 볼 예정입니다.

 

Database: 🐬 MySQL

 

 

  • build.gradle
dependencies {
	runtimeOnly 'com.mysql:mysql-connector-j'
}
  • application.yml
spring:
  datasource:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul
  username: DB 생성 시, 작성한 username
  password: DB 생성 시, 작성한 password

 

+ 리팩토링

spring:
  datasource:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Seoul
  username: DB 생성 시, 작성한 username
  password: DB 생성 시, 작성한 password
  • useSSL=false

Establishing SSL connection without server's identity verification is not recommended
최신 mysql버전을 사용 시, SSL연결을 기본으로 사용
SSL을 사용하지 않을 경우, 명시하지 않으면 경고가 발생하므로 이를 제거하기 위해 추가

 

 

 

기존 JPA 관련 설정은 다음과 같습니다.

application.yml

  jpa:
    # Application 구동할 때마다, Entity로 정의된 테이블을 생성
    generate-ddl: true
    hibernate:
      # 스키마 생성, 테이블 존재 시 변경된 부분만 반영
      ddl-auto: update
    show-sql: true
    database: mysql
    database-platform: org.hibernate.dialect.MySQL8Dialect
  • generate-ddl: true

데이터베이스 스키마를 자동으로 생성하거나 업데이트하는 기능을 활성화하는 설정

🚨 개발 및 테스트 환경에서 유용할 수 있지만, 운영 환경에서는 주의해서 사용

 

+ 리팩토링

  jpa:
    # Application 구동할 때마다, Entity로 정의된 테이블을 생성
    generate-ddl: true
    hibernate:
      # 스키마 생성, 애플리케이션 실행 시 모든 table trop 후 create
      ddl-auto: create
    # 실행 sql문 콘솔에서 확인
    show-sql: true
    # formatter
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true
        highlight_sql: true
    # using database
    database: mysql
    # mysql 상세 지정
    database-platform: org.hibernate.dialect.MySQL8Dialect

 

  • ddl-auto: update → create

개발 초기 단계이고, Entity 수정 예정이기에 애플리케이션 실행 시마다 table을 새롭게 만들고자 변경

  • formatter 추가

 

DDL이 출력된 모습이 다음과 같이 변경됩니다.

Hibernate: create table trip_schedule (schedule_id integer not null auto_increment, mod_date datetime(6), reg_date datetime(6) not null, user_id varchar(20) not null, city_name varchar(30), date_list varchar(500), primary key (schedule_id)) engine=InnoDB

 

+ 리팩토링

[Hibernate] 
    create table trip_schedule (
        schedule_id integer not null auto_increment,
        mod_date datetime(6),
        reg_date datetime(6) not null,
        user_id varchar(20) not null,
        city_name varchar(30),
        date_list varchar(500),
        primary key (schedule_id)
    ) engine=InnoDB

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

 

[SpringBoot] 스프링 부트 프로젝트 생성

👉 기본 환경 - Language: Java - DB: MySQL - IDE: IntelliJ 1. Spring Initializer에서 기본 설정 구축 https://start.spring.io/ - Project - Gradle - Groovy: 환경 설정 파일 build.gradle - Gradle - Kotlin: JVM에서 실행되는 다목적 프

hj0216.tistory.com

 

1.5 JDBC

1) JDBC란? JDBC 개요 JDBC(Java Database Connectivity)의 정의 - 자바를 이용한 데이터베이스 접속과 SQL 문장의 실행, 그리고 실행 결과로 얻어진 데이터의 핸들링을 제공하는 방법과 절차에 관한 규약 - 자

hoit1302.tistory.com

 

[Spring] hibernate 설정 정보

hibernate 설정 정보에 대해서

velog.io