스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 생성한다.

 

 

 

싱글톤 패턴

서블릿 클래스당 하나의 오브젝트만 만들어두고,

 

사용자 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용한다.

 

이렇게 애플리케이션 안에 제한된 수의 오브젝트만 만들어서 공유해서 사용하는 것이 싱글톤 패턴의 원리이다.

 

 

 

자바의 싱글톤 패턴을 구현하는 방법

1. 클래스 밖에서 오브젝트를 생성하지 못하게 private로 생성자를 만든다.

2. 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 static 필드를 정의한다.

3. 스태틱 팩토리 메소드인 getInstance() 를 만들고, 이 메소드가 최초로 호출되는 시점에서 한번만 오브젝트가 만들어지게 한다.

-> 생성된 오브젝트는 static 필드에 저장된다.

4. 싱글톤 오브젝트가 만들어지고 난 후에는 getInstance 메소드로 이미 만들어진 static 필드에 저장해둔 오브젝트를 넘김

 

public class Dao {
	private static Dao INSTANCE;
    
    private String name;
    
    private Dao(String name){
    	this.name = name;
    }
    
    public static synchronized Dao getInstance(String name){
    	if(INSTANCE == null) INSTANCE = new Dao(name);
        return INSTANCE;
    }
}

 

 

 

자바의 싱글톤 패턴의 단점

1) private 생성자를 갖고있어 상속할 수 없다.

2) 만들어지는 방식이 제한적이라 테스트하기 어렵다.

3) 서버환경에서는 여러 JVM에 분산돼서 설치되는 경우에도 각각의 독립된 오브젝트가 생성되 싱글톤 가치가 떨어진다.

4) 전역 상태로 사용되기 쉬워 객체지향에서 권장하지 않는 프로그래밍 방법이다.

 

 

 

싱글톤 레지스트리

하지만 스프링에서는 서버환경에서 싱글톤 방식을 적극 지지한다.

그리고 이를 스프링에서는 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다.

 

스프링 컨테이너는 싱글톤을 생성,관리,공급하는 싱글톤 관리 컨테이너의 역할도 한다.

이 스프링 컨테이너는 위의 static 메소드와 private 생성자 없이도 클래스를 싱글톤으로 활용할 수 있게 해준다.

그래서 관계설정, 컨테이너를 사용한 생성 등에 대한 제어권을 컨테이너에게 넘기는 이유중에 하나도 이 점 때문이다.

 

 

예제)

public class UserDao {

    private String test;

    public UserDao(String test) {
        this.test = test;
    }

}
@Component
public class DaoFactory {

    @Bean
    public UserDao userDao() {
        return new UserDao("test");
    }

}
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

        UserDao userDao1 = context.getBean("userDao", UserDao.class);
        UserDao userDao2 = context.getBean("userDao", UserDao.class);

        System.out.println("userDao1: " + userDao1);
        System.out.println("userDao2: " + userDao2);

        DaoFactory daoFactory = new DaoFactory();

        UserDao userDao3 = daoFactory.userDao();
        UserDao userDao4 = daoFactory.userDao();

        System.out.println("userDao3: " + userDao3);
        System.out.println("userDao4: " + userDao4);
    }

 

위 결과 값은 아래와 같다.

userDao1: com.example.demo.dto.UserDao@32502377
userDao2: com.example.demo.dto.UserDao@32502377
userDao3: com.example.demo.dto.UserDao@2c1b194a
userDao4: com.example.demo.dto.UserDao@4dbb42b7

 

스프링이 생성하는 빈 오브젝트는 모두 같은 객체임을 알 수 있고,

팩토리를 통해 빈 오브젝트를 생성하면 서로 다른 객체임을 알 수 있다.

 

 

싱글톤의 오브젝트 상태 관리

멀티스레드 환경에서 싱글톤 오브젝트는 여러 스레드가 동시에 접근해서 사용할 수 있다.

따라서 상태 관리에 주의를 요구한다.

 

if. 멀테스레드 환경에서 서비스 형태의 오브젝트를 사용하는 경우

then. 상태 정보를 내부에 갖고 있지 않은 stateless 방식으로 만들어져야한다.

 

그 이유는 다중 사용자 요청을 여러 스레드가 동시에 싱글톤 오브젝트 인스턴스 변수를 수정하는것은 위험하기 때문이다.

저장할 공간이 하난데 서로 값을 덮어쓰고 저장하지 않은 값이 읽어 올 수 있기 때문이다.

 

단, 읽기전용의 정보는 인스턴스 변수로 정의해서 사용하는 것에 문제가 없다.

 

* stateless: 인스턴스 필드의 값을 변경하거나 상태 유지를 하지 않는 방식

 

 

 

 

 

* 용어

스프링 프레임 워크

IoC 컨테이너를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 때 주로 사용한다.

 

 

IoC 컨테이너

애플리케이션 컨텍스트, 빈 팩토리랑 같은 의미로 표현된다.

 

 

팩토리

객체의 생성 방법을 결정하고, 그렇게 만들어진 객체를 돌려주는 역할을 하는 오브젝트

 

public class Factory {
	
    public UserDto getUserDto(){
    
        DbConnector connect = new DbConnector();

        UserDto user = new UserDto(connect);
        return userDto();
    }
}

 

Factory의 getUserDto를 호출하면 디비 커넥션 설정을 가져오고, user 오브젝트를 돌려주는 역할을 해준다.

 

이를 통해 클라이언트에서는 팩토리 클래스를 활용하면 디비 커넥션, 오브젝트 초기화 부분을 신경쓰지 않고 호출만 하면 된다.

 

 

 

이 팩토리의 역할을 어떻게 정의할 수 있을까?

애플리케이션을 구성하는 핵심적인 로직들의 관계를 정의하는 책임을 맡는 것이다.

즉, 컴포넌트의 구조와 관계를 정의하는 설계도와 같은 역할을 한다고 할 수 있다.

그래서 오브젝트가 어떤 오브젝트를 사용하는지를 정의하는 코드가 주로 들어간다.

 

 

 

제어의 역전이란?

모든 종류의 작업을 사용하는 쪽에서 제어하는 것이 아닌,

오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않고 팩토리와 같이 다른 오브젝트에게 모든 제어 권한을 위임하는 것을 의미한다.

 

예를 들어, 프로젝트의 시작을 담당하는 main 메소드는 엔트리 포인트를 제외하면 모든 오브젝트들에게 제어 권한을 위임한다.

 

대표적인 기술로 프레임워크가 있다.

프레임워크는 라이브러리들을 활용해 애플리케이션 흐름을 제어하도록 만들어진 반제품이라고 볼 수 있다.

이는 단지, 동작 중에 필요한 작업에 라이브러리를 사용하는 것일 뿐이다.

 

그리고 개발자는 그 프레임워크가 짜놓은 틀 안에서 애플리케이션 코드를 작성한다.

그러면 개발자는 코드를 작성할 뿐, 실제 애플리케이션을 직접 제어하는 것은 프레임워크가 되는 것이다.

 

 

 

Bean의 등장

팩토리를 이러한 원리를 가지고 스프링에 적용하면 어떻게 될까?

 

스프링에서는 스프링의 제어권을 가지고 직접 관계를 만들어 부여하는 오브젝트Bean 이라고 부른다.

이는 스프링 컨테이너가 생성, 관계 설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트이다.

 

그리고 이러한 빈의 생성과 관계 설정과 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리라고 부른다.

빈 팩토리를 활용한 대표적인 오브젝트로 애플리케이션 컨텍스트(ApplicationContext) 가 있다.

 

 

 

Application Context

이 오브젝트는 애플리케이션 전반에 걸쳐 모든 구성 요소의 제어 작업을 담당하는 IoC 엔진이라고 볼 수 있다.

 

예를 들어,

UserDto 와 DbConnection이 있을 때, UserDto와 DbConnection의 관계 설정을 어떻게 할 지를 제어한다.

하지만 애플리케이션 컨텍스트에서 이 정보를 직접 담고 있진 않다.

대신 설정 정보를 담고 있는 Config 클래스를 불러와서 활용하는 역할을 해준다.

 

이렇게 그 자체로는 애플리케이션 로직을 담당하고 있진 않지만 IoC 방식을 이용해

애플리케이션  컴포넌트를 생성하고, 사용할 관계를 맺어주는 책임을 담당하는 설계도와 같은 역할을 한다.

 

* 스프링에서는 이러한 설정 정보를 담당하는 애노테이션을 @Configuration 으로 표시한다.

@Configuration
public class dbConfig {
	//
    @Bean
    public DbConnection connection(){
    	return new DbConnection();
    }
    
 }

 

public class test(){
	//
    main(){
    	ApplicationContext context = new ApplicationContext(DbConfig.class);
    }
}

 

 

애플리케이션 컨텍스트의 장점

1. 클라이언트는 팩토리의 클래스를 알 필요가 없다.

클라이언트가 오브젝트를 가져올 때마다 팩토리 오브젝트를 생성하는 번거러움을 덜어준다.

그리고 아무리 팩토리가 많아져도 이를 알거나 직접 사용할 필요가 없다.

애플리케이션 컨텍스트가 알아서 가져와 준다.

 

 

2. 종합 IoC 서비스를 제공한다.

위와 같은 방식 외에도 오브젝트가 생성되는 방식, 시점, 전략을 다양하게 가져갈 수 있다.

 

 

3. 타입만으로 빈을 편리하게 검색할 수 있는 방법을 제공한다.

 

 

의존관계 주입

IoC는 스프링에서 아주 폭넓게 사용 되는 용어이다.

단순 서블릿 컨테이너인지, IoC 개념이 적용된 템플릿 메소드 패턴을 의미하는지, IoC 특징을 지닌 기술을 의미하는지 알 수 없다.

그래서 의존 관계 주입(DI) 이라는 명확한 이름을 사용하기 시작했다.

 

DI는 오브젝트 레퍼런스를 외부로부터 참조받고 이를 통해 다른 오브젝트와 의존관계가 만들어지는 것이 핵심이다.

 

 

의존관계

의존 관계란 누가 누구에게 의존하는 관계에 있다는 식이어야 한다. 즉, 방향성이 있다.

즉 "A가 B에 의존하고 있다" 라는 것은 "B가 무언가를 하면 A가 영향을 미친다" 라는 의미이다.

 

런타임 의존관계

런타임 시에 의존관계를 맺게 되는 특정 오브젝트를 의미한다.

 

의존 관계 주입의 세가지 조건

1. 클래스나 코드 속에는 런타임 시점의 의존관계가 드러나지 않는다. (인터페이스에만 의존해야 한다)

2. 런타임 시점의 의존관계컨테이너, 팩토리 같은 제3의 존재가 결정한다.

3. 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.

 

 

 

 

개요

스프링을 이해하려면 오브젝트에 관심을 가져야 한다.

오브젝트가 생성되고, 관계를 맺고, 소멸되기까지의 과정을 이해하고 생각해야 한다.

더 나아가 오브젝트가 어떻게 설계되고, 어떤 단위로 만들어지는지, 언제 존재를 나타내는지에 대해서도 살펴봐야 한다.

 

스프링은 이러한 오브젝트를 누구나 솝쉽게 적용할 수 있도록 프레임워크 형태로 제공한다.

 

그리고 객체는 항상 변화한다. 그리고 개발자는 변화하는 객체를 위해 미래지향적으로 설계를 해야한다.

객체에 대한 작업이 한 곳에 집중될 경우 매번 분리와 확장에 골머리를 썩을 수 있다.

 

그래서 관심사의 분리가 필요하고, 그 관심사의 분리를 위해 알아야할 몇가지가 있다.

 


인터페이스

- 클래스 간 서로 긴밀하게 연결되지 않도록 중간에 추상적인 연결고리를 만들어 주는것

- 자신을 구현할 클래스에 대한 구체적인 정보는 감춰야함

- 어떤 것을 하겠다는 기능만 정의해 놓은 것 -> 즉, 어떻게 구현하겠다는 것은 명시되어 있지 않음

 

* 추상화

공통적인 성격을 뽑아내여 따로 분리하는 작업

 

 


관심사의 분리 예제

가정.

- UserDao: 유저 정보를 담은 도메인 존재

- DbConnection: DB 커넥션을 설정하는 Config 존재

 

UserDao에서 DbConnection과의 관계를 설정하는 메소드가 있다면, 이것은 독립적으로 확장 가능한 클래스가 아니다.

이는 어떻게 분리할 수 있을까?

 

UserDao의 클라이언트 오브젝트에서 DbConnection과 UserDao의 관계를 결정하는 기능을 두는 것이다.

이렇게 되면 UserDao와 DbConnection 사이의 기능을 완전히 분리시킬 수 있다.

 

방법.

1. 서비스의 파라미터로 인터페이스를 받게함

2. 서비스의 클라이언트에서 인터페이스의 구현체를 생성

3. 그 구현체를 서비스를 생성하면서 파라미터로 넘겨주는 것

 


왜 이러한 관심사의 분리가 필요한가?

 

높은 확장성을 지니기 위해서이다.

생각해보자. 만약, UserDao 생성자에특정 DB와의 커넥션을 생성하도록 구현되어 있다면 다른 도메인들을 생성할때마다 DB 커넥션 관련 코드를 작성해야한다.

이는 중복 코드를 유발하고, 낮은 유지보수성을 가지게 된다. 

 


OCP (개방 폐쇄의 원칙)

 SOLID 원칙 중에 하나

(SRP - 단일 책임 원칙, OCP, LSP - 리스코프 치환 법칙, ISP - 인터페이스 분리 법칙, DIP - 의존관계 역전 원칙)

 

클래스나 모듈의 확장은 열려있어야 하고, 변경에는 닫혀 있어야한다.

위의 예시를 보면, 클라이언트에서 Db커넥션 구현체를 늘렸을 때, UserDao의 도메인에는 전혀 영향을 주지않게 된다.

 

이러한 특징을 우리는 높은 응집도와 낮은 결합도와 연관 지을 수 있다.

 

높은 응집도

 

변화가 일어날 때 모듈에서 변하는 부분이 크다.

즉, 변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높다고 할 수 있다.

만약, 도메인 내부 응집도가 낮다면 문제가 발생했을 때 일일이 디버깅해야하는 귀찮은 일이 생긴다.

예를 들어, 빵을 주문하는 서비스 하나에

 

- 빵 재고 확인

- 빵 상태 확인

- 빵 주문

 

각 서비스가 낮은 응집도를 지닌다면 빵의 주문 시스템이 변경될 때마다 일일이 수정사항을 찾아 나가야 할 것이다.

 

낮은 결합도

 

책임과 관심사가 타 모듈과 낮은 결합도를 가지게 되는 것. 즉, 느슨한 연결 형태.

그래서 구현체들은 독립적이며, 서로 알 필요가 없어지게 된다.

그래서 높은 확장성을 지니게 된다.

이는 위의 예제에서 나타내고 있다.

 

 

 

 

 

 

 

 

Spock Framework

 

개요 및 특징
  • Groovy 언어를 기반으로 자바와 100% 호환됨
  • mocking, stubbing 을 지원해 mock객체로 동작 정의가 쉬움
  • 라이프사이클 메소드 제공 : setup(), cleanup() 등의 메소드로 테스트 전/후 설정 수행이 가능
  • 동일한 데이터를 여러번 실행가능한 블록 제공
  • 테스트 블록을 분리하고 실행 순서 지정 가능

 

 

Spock Block
  • given / setup
    • 테스트에 필요한 객체나 환경을 준비하는 블록
  • when
    • 테스트 하고 싶은 상황을 만드는 영역 ( 코드 실행 영역 )
  • then
    • 테스트 결과를 검증하는 블록 ( assert에 해당하는 문장 )
  • expect
    • when, then이 합쳐진 형태로 단위 테스트 시 유용
  • cleanup
    • 필요시 setup 자원을 정리
  • where
    • 일부 데이터만 바꿔가며 테스트를 할 수 있게 도와주는 영역

 

 

기본 사용법

 

1. 라이브러리 추가

testImplementation "org.spockframework:spock-spring"

 

2. 플러그인 추가

apply plugin: 'groovy' // groovy 지원

 

3. Specification 클래스 상속

import spock.lang.Specification

class UpdateBizFleetOrderVinsToPurchasedHandlerTest extends Specification {
}

* 이때, 주의할 점은 클래스를 ctrl + command + T 로 만들 때, 기존에 Test Library JUnit으로 사용하시던 분들은 Groovy로 바꿔줘야 합니다.

 

 

4. 라이프 사이클에 맞는 메소드 생성

def setupSpec() {}    // 모든 테스트 케이스 실행 전 실행 (@BeforeClass)
def setup() {}        // 각 테스트 케이스 실행 전 마다 실행 (@Before)
def cleanup() {}      // 각 테스트 케이스 실행 후 마다 실행 (@After)
def cleanupSpec() {}  // 모든 테스트 케이스 실행 후 실행 (@AfterClass)

 

 

참고
  • @Shared
    • 여러 테스트 간에 걸쳐서 공유되는 객체
  • Mock 테스트 코드
    • 도메인 클래스를 Mock 클래스의 피라미터로 주면서 생성
    • >> 를 통해 값을 반환 ( ex. A.findName(1) >> "park" )

에러 :

Caused by: java.io.FileNotFoundException: class path resource [elastic/article-setting.json] cannot be opened because it does not exist

 

말 그래도 경로를 못잡아서 생기는 문제입니다.

 

저 같은 경우,

@Mapping(mappingPath = "elastic/article-mapping.json")
@Setting(settingPath = "elastic/article-setting.json")
public class ArticleDoc {
...
}

이렇게 경로를 잡고 있는데, resource 경로에 파일명도 제대로 되있는데 왜 못잡을까? 를 생각했습니다.

 

정적 파일이 resource/static/elastic/~~.json 과 같이 원래 잡던 경로랑 달라져서 생기는 문제였습니다.

 

아래와 같이 문제를 해결할 수 있습니다.

 

1. WebConfig 설정

package com.example.elasticsearch.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        String[] resourceLocation = {
                "classpath:/static/", "classpath:/resources/","classpath:/public/","classpath:/static/elastic/"
        };
        registry.addResourceHandler("/**").addResourceLocations(resourceLocation);
        super.addResourceHandlers(registry);
    }
}

 

 

2. Setting 경로 수정

@Mapping(mappingPath = "/static/elastic/article-mapping.json") // 경로수정
@Setting(settingPath = "/static/elastic/article-setting.json") // 경로수정
public class ArticleDoc {
...
}

 

 

이렇게 하면 문제가 해결되는 것을 볼 수 있습니다.

 

 

!! 참고

https://tjdwns4537.tistory.com/158

 

정적 리소스 사용하기 ( Resource Handler )

상황 : Controller 에서 Templates 경로의 View 를 반환하는데 404가 발생 다음의 상황에서 localhost:8080/testMain 를 접속하면 404가 뜨는 것을 볼 수 있습니다. 이 경우, 다음과 같은 설정을 해줘야 합니다. 위

tjdwns4537.tistory.com

 

@Valid  로 검증을 할 때 발생하는 에러

 

생각보다 간단한 에러입니다.

 

[ 문제 상황 ]

1. 회원가입 페이지의 Domain에 이름과 연락처 등에 대해 @NotEmpty 을 붙쳐줬습니다.

2. 컨트럴러에서 @Valid, BindingResult 피라미터를 넣어줍니다.

3. 검증에러가 발생시 bindingResult 객체에 에러가 담기고, 에러가 발생하면 다시 재입력받을 수 있는 페이지를 구축하는 것입니다.

 

* 발생한 문제: 검증이 발생하는 것은 파악이 되지만, bindingResult 객체가 제대로 작동하지 않음

 

 

 

 

[ 문제 해결 ]

검증은 피라미터 순서에 영향을 받습니다.

따라서

@PostMapping("/join")
    public String joinExecute(
            @Valid MemberForm memberForm,
            @RequestParam String password2,
            RedirectAttributes redirectAttributes,
            Model model,BindingResult bindingResult)

위의 코드처럼 @Valid 후 BindingResult 순서가 바로 붙어있는게 아닌 저렇게 다른 피라미터를 먼저 받을 경우 원하는 동작을 안합니다.

 

@PostMapping("/join")
    public String joinExecute(
            @Valid MemberForm memberForm,BindingResult bindingResult,
            @RequestParam String password2,
            RedirectAttributes redirectAttributes,
            Model model)

이렇게 피라미터 순서를 제대로 배치하면 해결됩니다.

발생하고 있던 문제 :

Button type = "submit" 으로 버튼 이벤트를 주다보니 다른 페이지로 이동해버리는 문제가 발생

 

 

[ 해결 완료 ]

 

이전 블로그에서는 다른 팝업창을 띄워서 그 곳에서 인증하는 방식으로 해결한다고 말했었습니다.

하지만 더 간단하고 쉬운 방법을 찾았습니다.

 

<form th:action="@{/member/emailConfirm}" method="post" target="param">
            <div>
                <h3 class="join_title"><label for="email">이메일</label></h3>
                <span class="box int_email">
                        <input type="text" name="email" id="email"
                               class="form-control" maxlength="100">
                    </span>
            </div>

            <!-- 이메일 인증 번호 요청 버튼 -->
            <input type="submit"
                   value="인증번호받기"
                    id="email-btn"
            />
</form>
<iframe id="if" name="param"></iframe>

위와 같이 iframe 을 활용하여 간단하게 해결할 수 있었습니다.

 

iframe 에 대한 설명은 아래의 글이 잘 적혀있어 읽어보시고 활용하면 좋을것 같습니다.

 

https://yeoulcoding.me/143

 

[HTML] iframe 태그란?

Definition iframe이란 Inline Frame 의 약자로, 웹 브라우저 내에 또 다른 프레임, 즉 현재 브라우저에 렌더링되고 있는 문서 안에 또 다른 HTML페이지를 삽입할 수 있도록 하는 기능을 제공합니다. 검색

yeoulcoding.me

 

 

만들려는 기능은 다음과 같습니다.

 

  1. 이메일 입력
  2. 인증번호 요청 클릭
  3. tjdwns4537@naver.com 이메일에 인증번호 전송
  4. 전송된 인증번호를 입력창에 입력 후 정상적으로 인증됬는지 확인

 

[ Controller ]

@PostMapping("/emailConfirm")
@ResponseBody
public void emailVerify(@RequestParam String email) throws Exception {
        log.info("post email = {}", email);
        emailService.sendSimpleMessage(email);
}

 

[ EmailService ]

package smilegate.securitySystem.service.EmailService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Random;

@Service
public class EmailServiceImp implements EmailServiceInterface{

    @Autowired
    JavaMailSender emailSender;

    public static final String emailPassword = createKey();

    public EmailServiceImp(JavaMailSender emailSender) {
        this.emailSender = emailSender;
    }

    public String getEmailPassword() {
        return emailPassword;
    }

    public static String createKey() {
        StringBuffer key = new StringBuffer();
        Random rnd = new Random();

        for (int i = 0; i < 8; i++) { // 인증코드 8자리
            int index = rnd.nextInt(3); // 0~2 까지 랜덤

            switch (index) {
                case 0:
                    key.append((char) ((int) (rnd.nextInt(26)) + 97));
                    //  a~z  (ex. 1+97=98 => (char)98 = 'b')
                    break;
                case 1:
                    key.append((char) ((int) (rnd.nextInt(26)) + 65));
                    //  A~Z
                    break;
                case 2:
                    key.append((rnd.nextInt(10)));
                    // 0~9
                    break;
            }
        }
        return key.toString();
    }

    @Override
    public String sendSimpleMessage(String to)throws Exception {
        // TODO Auto-generated method stub
        MimeMessage message = createMessage(to);
        try{//예외처리
            emailSender.send(message);
        }catch(MailException es){
            es.printStackTrace();
            throw new IllegalArgumentException();
        }
        return emailPassword;
    }

    private MimeMessage createMessage(String to)throws Exception{
        System.out.println("보내는 대상 : "+ to);
        System.out.println("인증 번호 : "+ emailPassword);
        MimeMessage message = emailSender.createMimeMessage();

        message.addRecipients(MimeMessage.RecipientType.TO, to);//보내는 대상
        message.setSubject("회원가입 이메일 인증");//제목

        String msgg="";
        msgg+= "<div style='margin:100px;'>";
        msgg+= "<h1> 안녕하세요 Sungjun 입니다. </h1>";
        msgg+= "<br>";
        msgg+= "<p>아래 코드를 회원가입 창으로 돌아가 입력해주세요<p>";
        msgg+= "<br>";
        msgg+= "<p>감사합니다!<p>";
        msgg+= "<br>";
        msgg+= "<div align='center' style='border:1px solid black; font-family:verdana';>";
        msgg+= "<h3 style='color:blue;'>회원가입 인증 코드입니다.</h3>";
        msgg+= "<div style='font-size:130%'>";
        msgg+= "CODE : <strong>";
        msgg+= emailPassword+"</strong><div><br/> ";
        msgg+= "</div>";
        message.setText(msgg, "utf-8", "html");//내용
        message.setFrom(new InternetAddress("kktd4537@gmail.com","SungJun"));//보내는 사람

        return message;
    }
}

 

[ Form 태그 ]

 

<form th:action="@{/member/emailConfirm}" method="post">
            <div>
                <h3 class="join_title"><label for="email">이메일</label></h3>
                <span class="box int_email">
                        <input type="text" name="email" id="email"
                               class="form-control" maxlength="100">
                    </span>
            </div>

            <button type="submit"
                    id="email-btn"> 인증번호 요청
            </button>
        </form>

        <div>
            <h3 class="join_title">인증 번호</h3>
            <input type="text" name="email-check" id="email-check" class="form-control" maxlength="100" placeholder="인증번호 입력">
        </div>

 

메일은 정상적으로 보내지는게 확인이 되지만

위의 코드에서 문제점이 있습니다.

 

코드를 보면 Form action 태그를 통해 member/emailConfirm 으로 url 을 요청하기 때문입니다.

 

그렇다고 회원가입 페이지인 member/join 에 다시 보내게 된다면 어떻게 될까요?

email 인증이 아닌 회원가입 버튼의 주소가 member/join 으로 되있기 때문에 PostMapping 메서드 두 개가

서로 같은 곳을 경로를 바라보게 됩니다.

 

그래서 생각한 해결방안은 다음과 같습니다.

 

[ 해결방안1 ]

1) 인증번호 요청 버튼 클릭

2) 자바스크립트를 이용해 팝업창 생성

3) 팝업창에서 인증번호에 대한 검증을 진행

4) 검증이 정상적으로 완료된다면 회원가입 페이지에 검증 완료 메세지 전송

 

[ 해결방안2 ]

1) 인증번호 요청 버튼 클릭

2) RestController 에 구현한 다른 PostMapping 메서드를 호출

3) 해당 메서드에서 검증기능 구현 후 검증이 완료 된다면 회원가입 페이지에 검증 완료 메세지 전송

 

사실 해결방안1에 대한 설계는 머릿속에 바로 들어오지만,

해결방안2에 대한 구조는 저도 어떻게해야 할지 잘 생각이 나지 않습니다.

 

그래서 해결방안1에 대해 먼저 진행해보도록 하겠습니다.

 

[ 구현 과정 ]

1. 컨트롤러 / 도메인/ 레포지토리 생성

2. 타임리프를 활용해 동적 HTML 생성

 

 

컨트롤러와 타임리프의 실행 과정
package springmvc.springmvc.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import springmvc.springmvc.domain.Member;
import springmvc.springmvc.repository.MemberRepositoryInterface;

import javax.annotation.PostConstruct;
import java.util.List;

@Slf4j
@Controller
@RequestMapping("/join")
@RequiredArgsConstructor
public class joinController {

    private final MemberRepositoryInterface memberRepository;

    @GetMapping("/memberView")
    public String memberListView(Model model) { // 회원 목록 조회
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members);
        log.info("info log={}",model.getAttribute("members"));

        return "/join/memberView";
    }

    @PostConstruct
    public void init() { // 테스트용 멤버 데이터 넣기
        memberRepository.save(new Member("성준","tjdwns", "1234", 010));
        memberRepository.save(new Member("수진","sujin", "2345", 011));
        memberRepository.save(new Member("성형","tjdgud", "0101", 533));
    }

    @GetMapping("/memberView/{id}")
    public String item(@PathVariable Long id, Model model) {
        Member findMember = memberRepository.findById(id);
        model.addAttribute("member", findMember);
        return "/join/member";
    }
}

 

@RequestMapping("/join")

회원가입과 관련된 HTTP url은 모두 join/ 로 시작하기때문에 join을 넣어놨습니다.

 

 

@PostConstruct
    public void init() { // 테스트용 멤버 데이터 넣기
        memberRepository.save(new Member("성준","tjdwns", "1234", 010));
        memberRepository.save(new Member("수진","sujin", "2345", 011));
        memberRepository.save(new Member("성형","tjdgud", "0101", 533));
}

테스트를 위해 프로젝트 생성시 멤버 리포지토리에 위의 멤버를 넣습니다.

 

 

@GetMapping("/memberView")
    public String memberListView(Model model) { // 회원 목록 조회
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members);
        log.info("info log={}",model.getAttribute("members"));

        return "/join/memberView";
}

1) /join/memberView 라는 url 을 접속

2) 멤버 리포지토리에서 findAll을 통해 회원 리스트를 생성

3) 모델에 members 라는 이름으로 회원 리스트를 저장

 

 

<tr th:each="member : ${members}">
                <td><a href="member.html" th:href="@{/join/memberView/{id}(id=${member.id})}" th:text="${member.id}">회원 식별번호</a></td>
                <td th:text="${member.memberName}">이름</a></td>
                <td th:text="${member.memberId}">아이디</a></td>
                <td th:text="${member.phoneNumber}">휴대폰 번호</td>
                <td th:text="${member.memberPassword}">password</td>
 </tr>

1) 타임리프를 활용해 모델에 담겨져있는 members 리스트를 순환

2) 각각의 리스트 목록에 [ 클래스.멤버 ] 로 모델에 담긴 데이터를 출력합니다.

3)

th:href="@{/join/memberView/{id}(id=${member.id})}" th:text="${member.id}"

/join/memberView 라는 브라우저 Url 에서 member.id 로 받아온 회원 식별번호를 뒤에 붙쳐서

/join/memberView/회원식별번호 로 링크를 걸어줍니다.

 

 

 

[ 출력 화면 ]

위의 화면에서 식별 번호에 링크를 누르면

 

회원 정보가 출력됩니다.

위와 같은 에러가 발생한 이유는 Database 에 연결할 필요 정보가 없기 때문입니다.

 

[ 해결방법 ]

 

위의 에러는 appllication.properties 에 데이터베이스에 대한 설정을 해줘야합니다. 아래 블로깅을 보시면 보실 수 있습니다.

 

https://tjdwns4537.tistory.com/117

 

Spring Boot 와 JPA( MySQL ) 연동 간단 가이드

* JPA 와 스프링 부트로 H2Database 를 사용해봤다는 가정하에 작성하였습니다. 1. 의존성 추가 org.springframework.boot spring-boot-starter-data-jpa mysql mysql-connector-java 2. application.properties 설정 추가 server.address=

tjdwns4537.tistory.com

 

 

그런데 여기서 의문점은 저는 jpa 와 데이터베이스를 사용하지 않고 순수 자바로만 레포지토리를 구현했는데 왜 이러한 에러가 뜰까?

였습니다.

 

단순히 강의를 따라하다보니 발생한 문제라고 생각하여 그 원인을 찾기로 하였습니다.

 

[ 의문점 해결방법 ]

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

@Data  를 붙친다는게 실수로

@Entity 를 넣었기 때문입니다.

 

엔티티 어노테이션을 붙치면서 라이브러리에 위의 jpa 라이브러리를 무의식 중에 적용시켰습니다...

그래서 jpa 관련 설정을 찾는 에러가 발생했습니다.

위의 라이브러리를 지워우고 다시 적용시켜주면 에러가 해결됩니다.

 

이번에 해볼 상황은 다음과 같습니다.

 

HTML Form 을 통해 회원 데이터를 가입하면, 새로운 페이지에서 그 데이터를 보여주는 것입니다.

 

 

Form Servlet 구현

package servletTest.servlet.webServlet;

import servletTest.servlet.repository.MemberRepository;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class memberFormServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");

        PrintWriter w = response.getWriter();
        w.write("<!DOCTYPE html>\n" +
                "<html>\n" +
                "<head>\n" +
                "    <meta charset=\"UTF-8\">\n" +
                "    <title>Title</title>\n" +
                "</head>\n" +
                "<body>\n" +
                "<form action=\"/servlet/members/save\" method=\"post\">\n" +
                "    username: <input type=\"text\" name=\"username\" />\n" +
                "    age:      <input type=\"text\" name=\"age\" />\n" +
                " <button type=\"submit\">전송</button>\n" + "</form>\n" +
                "</body>\n" +
                "</html>\n");

    }
}

url 주소 : /servlet/members/new-form

해당 주소에 접속하게 되면 response 에 html 형태로 응답을 보내게 됩니다.

 

form action 에 데이터를 보낼 ur 을 작성해줍니다.

 

Form 데이터 받아서 출력하기

package servletTest.servlet.webServlet;

import servletTest.servlet.domain.member.Member;
import servletTest.servlet.repository.MemberRepository;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class memberSaveServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void  service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("MemberSaveServlet.service");

        // 요청 데이터
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        // 저장
        Member member = new Member(username, age);
        memberRepository.save(member);

        // 응답 메시지
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");
        PrintWriter writer = response.getWriter();
        writer.write("<html>\n" +
                "<head>\n" +
                " <meta charset=\"UTF-8\">\n" + "</head>\n" +
                "<body>\n" +
                "성공\n" +
                "<ul>\n" +
                "    <li>id="+member.getId()+"</li>\n" +
                "    <li>username="+member.getUsername()+"</li>\n" +
                " <li>age="+member.getAge()+"</li>\n" + "</ul>\n" +
                "<a href=\"/index.html\">메인</a>\n" + "</body>\n" +
                "</html>");
    }
}

받아온 데이터를 Member 객체에 저장해주고, 싱글톤으로 구현한 Repository 를 통해 저장해줍니다.

 

그리고 받아온 데이터를 응답메시지로 다시 HTML 형태로 출력해줍니다.

데이터를 전송을 하면

성공적으로 데이터가 출력이 됨을 볼 수 있습니다.

 

 

마무리 정리하면...

Resource 아래에 Html 파일을 작성하여 Form 을 구성할 수 있습니다.

하지만 왜 굳이 서블릿을 통해 불편하게 HTML 파일을 작성할까요?

 

그 이유는 동적으로 HTML 파일을 생성할 수 있기 때문입니다.

즉, 이미 만들어진 형태가 아닌 원하는 형태로 원할 때, 원하는 구성으로 HTML 파일을 구성할 수 있다는 장점입니다.

 

그래서 계속 달라지는 저장 결과나, 회원 목록 같은 동적인 HTML 만드는 일이 가능해집니다.

 

하지만 위의 코드를 보듯이 매우 비효율적인 코드 작성을 하게 됩니다.

 

그래서 HTML 파일 안에 변경해야하는 부분만 자바 코드를 넣을 수 있도록 한 것이 템플릿 엔진입니다.

 

템플릿 엔진에는 [ JSP , Thymeleaf, Freemarker 등 ] 이 있습니다.

 

하지만 JSP 는 현재 점점 없어지는 추세이기 때문에 Spring MVC 패턴을 가지고 토이 프로젝트를 진행하도록 하겠습니다.

 

HttpServletRequest 역할

- HTTP 를 직접 파싱하는 경우

: 메시지를 개발자가 직접 파싱해서 사용해도 되지만, 매우 할 일이 많아집니다.

 

- 서블릿 필요 이유

서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자를 대신해서 HTTP 요청 메시지를 파싱합니다.

 

- HttpServletRequest 의 존재 이유

그 요청 메시지에 대한 결과를 HttpServletRequest 객체에 담아서 제공합니다.

 

 

 

HttpServletRequest 예시

- HTTP 요청 메시지

POST /save HTTP/1.1
Host : localhost: 8080
Content-Type : application/x-www-form-urlencoded

username=park&age=20

이와 같은 요청 메시지를 직접 매번 파싱한다는 의미입니다. 

 

각 문장에 대해 HttpServletRequest 가 하는 역할은

- 첫 번째 줄 : START LINE [ HTTP 메소드, URL, 스키마 ]

- 두, 세 번째 줄 : 헤더 [ 헤더 조회 ]

- 바디 : 한 줄 띄우고 나머지 부분 [ form 피라미터 형식 조회, 메세지 바디 데이터 직접 조회 ]

 

 

 HttpServletRequest 객체의 여러가지 기능

- 임시 저장소 기능

HTTP 요청이 시작부터 끝날 때 까지 유지되는 임시 저장소 기능

request.setAttribute ( name, value )

request.getAttribute ( name )

 

- 세션 관리 기능

request.getSession ( create: true )

 

 

HTTP Request 해보기

PostMan 설치 후 HTTP 메서드를 테스트 해봤습니다.

위와 같이 Request 메서드에 대해 print 할 수 있는 메서드를 작성하였습니다.

 

그 후 POST 로 메세지를 보내면

위와 같이 요청 메시지가 정상적으로 들어옴을 확인할 수 있습니다.

ㅇ WAR 로 패키징하기 ( Spring Initailize )

JAR 가 아닌 WAR 로 패키징하면 톰캣에 직접 넣을 수도 있고, 스프링 부트의 내장 톰캣에서 띄울 수도 있습니다.

그래서 서블릿으로 페이지를 구현하려면 WAR 를 사용해야 합니다.

 

 

ㅇ @ServletComponentScan

서블릿을 찾아 자동으로 등록해줍니다.

 

 

 

ㅇ HttpServlet 상속받기

- 서블릿을 구현하려면 HttpServlet 을 상속받아야 합니다.

 

 

 

ㅇ WebServlet ( name = " Servlet name ", urlPatterns = " url 경로 " )

서블릿의 Url 경로를 설정

 

 

 

ㅇ 서비스 메서드 생성

서블릿이 호출되면 서비스 메서드가 호출 됩니다.

 

 

 

ㅇ 서블릿 요청 및 응답 해보기

- 서블릿 요청 :

String username = request.getParameter("username");

위 메서드를 통해 

http://localhost:8080/hello?username=kim 를 브라우저에 입력하게 되면

브라우저에 입력된 값이 요청되는 것을 알 수 있습니다.

 

 

 

- 서블릿 응답 :

response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.getWriter().write("hello" + username);

위 메서드를 통해

이러한 응답 메세지가 전송됨을 알 수 있습니다.

 

 

 

ㅇ 개발 단계에 로깅 찍어보기

logging.level.org.apache.coyote.http11=debug

 

 

 

 

ㅇ 인덱스 페이지 만들어보기

webapp 경로에 index.html 생성

상황 : Controller 에서 Templates 경로의 View 를 반환하는데 404가 발생

다음의 상황에서 localhost:8080/testMain 를 접속하면 404가 뜨는 것을 볼 수 있습니다.

이 경우, 다음과 같은 설정을 해줘야 합니다.

위의 설정을 하나하나 뜯어보도록 하겠습니다.

 

WebMvcConfigurer

WebMvcConfigurer 을 사용하면 @EnableWebMvc 가 자동적으로 세팅해주는 설정에 개발자가 원하는 설정을

추가할 수 있습니다.

 

 

@EnableWebMvc

Spring Framework 에서 여러 Config 값을 알아서 세팅해줍니다.

 

 

addResourceHandler

정적인 Resource 를 처리하기 위해 사용되는 Handler 입니다.

기본적으로 서블릿 컨테이너에는 정적인 자원을 처리할 수 있는 서블릿이 등록되어 있습니다.

그래서 프로젝트를 생성하고 아무 설정을 안해도 리소스 요청에 동작을 합니다. ( ex. static Directory )

 

하지만, 특정 요청에 대한 리소스를 컨트롤해야 한다면 리소스 핸들러를 정의해서 config 에 등록해줘야 합니다.

이때, addResource Handler 메서드를 통해 어느 경로로 들어왔을 때 매핑이 되어줄 것인지를 정의해줍니다.

registry.addResourceHandler("/static/**")

이후 addResourceLocations 메서드를 통해 실제 파일이 있는 경로를 설정해줍니다.

그래서 보면 Main 경로가 있는 templates 와 css/js 파일이 있는 static 을 경로 설정을 해줬습니다.

만약, static 경로를 css / js 등등을 따로 나눠서 잡아야 한다면 다음과 같이 설정해줘야 합니다.

registry.addResourceHandler("/css/**").addResourceLocations("classpath:/css").setCachePeriod(false)
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/js").setCachePeriod(false)

 

첫번째 오류

 

 

[ 환경 ] 

- 스프링 부트

- 자바11

- JPA + MySQL

- Gradle

- Application.properties 설정

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/~~?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=??
spring.datasource.password=??
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# mysql 사용
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=none
spring.jpa.generate-ddl=false

# 로깅 레벨
logging.level.org.hibernate=info

# 하이버네이트가 실행한 모든 SQL문을 콘솔로 출력
spring.jpa.properties.hibernate.show_sql=true
# SQL문을 가독성 있게 표현
spring.jpa.properties.hibernate.format_sql=true
# 디버깅 정보 출력
spring.jpa.properties.hibernate.use_sql_comments=true

 

 

두번째 오류

Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set

 

 

[ 해결방법 ]

두 오류를 종합해봤을 때, application.properties 의 MySQL 설정을 읽지 못하고 있는 것으로 보입니다.

그 이유는 제가 META-INF 에 PersistenceUnit 을 설정하기위해 persistence.xml 을 만들어 놓고

내부 태그에 아무 내용을 적지 않았기 때문입니다.

 

=> persistence.xml 에 다음과 같은 코드를 작성 후 해결완료

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             version="2.1">
    <persistence-unit name="Recycler">

        <class>kbbank.recycler.domain.MEMBER</class>

        <properties>
            <property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="javax.persistence.jdbc.user" value=""/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://127.0.0.1:3306/Recycler"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>

            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
            <property name="hibernate.jdbc.batch_size" value="10"/>
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <!--property name="hibernate.id.new_generator_mappings" value="true"/-->
        </properties>
    </persistence-unit>

</persistence>

* JPA 와 스프링 부트로 H2Database 를 사용해봤다는 가정하에 작성하였습니다.

 

1. 의존성 추가

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

	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
	</dependency>
</dependencies>

 

2. application.properties 설정 추가

server.address=localhost
server.port=8080

spring.datasource.url=jdbc:mysql://localhost:3306/TEST_DB?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=test_user
spring.datasource.password=admin
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# mysql 사용
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

# 로깅 레벨
logging.level.org.hibernate=info

# 하이버네이트가 실행한 모든 SQL문을 콘솔로 출력
spring.jpa.properties.hibernate.show_sql=true
# SQL문을 가독성 있게 표현
spring.jpa.properties.hibernate.format_sql=true
# 디버깅 정보 출력
spring.jpa.properties.hibernate.use_sql_comments=true

 

 

3. Entity 생성

 

4. Repository 생성

 

5. Controller 생성

 

6. 결과 확인

테스트 코드에서 위 사진과 같은 에러가 발생하고 있습니다.

 

  1. 멤버 리포지토리에 멤버 객체를 저장 ( java memory 사용 )
  2. findById 로 저장한 객체를 꺼내와서 비교

간단하게 위 두 과정을 거치는데 null 값을찾고 있다는 오류가 발생해 디버깅을 해보니

 

 

저는 분명 하나의 객체를 저장했는데 sequence 는 2를 가리키고 있습니다.

 

 

[ 해결 ]

간단하게 테스트하기 위해 save 할 때 , Id 값을 직접 지정해주도록 했습니다.

그런데 member 객체에 Id 값을 설정안해주고, member.getId() 메서드로 Id를 호출하니 발생하는 오류 였습니다.

 

해결방법 : 테스트 코드의 member 객체에 Id 값을 설정해줘야함

Spring Bean

스프링 컨테이너에 의해 관리되는 자바 객체를 의미합니다.

 

 

Spring Bean 을 왜 사용하는가?

개발자는 new 키워드, Interface 호출, 팩토리 호출 방식으로 객체를 생성하고 소멸합니다.

하지만 Spring Container를 사용하면 이러한 역할을 대신 해줍니다.

1) 제어 흐름을 외부에서 관리하게 됩니다.

2) 객체들 간의 의존 관계를 스프링 컨테이너가 런타임 과정에서 알아서 생성해줍니다.

 

 

Spring Bean 등록 방식

1. Component Scan

 

- @Component 를 클래스 위에 명시하면 스프링이 알아서 등록해줍니다.

- @Controller, @Service, @Repository, @Configuration 은 @Componenet를 상속 받고 있으므로

   모두 컴포넌트 스캔 대상입니다.

 

- Controller : Spring MVC Controller
- Repository : 스프링 데이터 접근 계층으로 해당 계층의 예외는 모두 DataAccessException 으로 변환
- Service : 핵심 비지니스로 인식 ( 가독성 역할 )
- Configuration : Spring 설정 정보로 인식하고, 스프링 빈이 싱글톤 유지를 하도록 처리해줌

 

  •   컴포넌트 스캔 방법1 ( @Component를 붙쳐주는 방법 )

1) 각 서비스/레포지토리 등에 어노테이션을 달아줌 ( @Repository, @Service, @Controller )

@Service
public class MemberServiceImp implements MemberServiceInterface{
    
    private MemberRepository memberRepository;

    @Autowired
    public MemberServiceImp(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    // 회원가입
    @Override
    public Long join(Member member) {
        // 휴대폰 번호 중복 체크
        memberRepository.save(member);
        return member.getId();
    }
}
@Repository
public class JPQLMemberRepository implements MemberRepository{
    
    public Member save(Member member) {
        return member;
    }
}

 

2) 빈 등록이 됬으면 의존성 주입

 

 

  •   컴포넌트 스캔 방법2 ( @Bean를 붙쳐주는 방법 )

1) IoC컨테이너에 등록을 해줌 ( @Configuration )

@Configuration
public class SpringConfig {
	
    //등록
    @Bean
    public EntityManager getEm() {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("5xik");
        return emf.createEntityManager();
    }
    
    @Bean
    public MemberServiceInterface memberService() {
        return new MemberServiceImp(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new JPQLMemberRepository(getEm());
    }
}

2) Repository / Serivce 에 필요한 의존성 주입

public class JPQLMemberRepository implements MemberRepository{

    private EntityManager em;

	@Autowired
    public JPQLMemberRepository(EntityManager em) {
        this.em = em;
    }
    
    public EntityTransaction getTx() {
        return em.getTransaction();
    }

    public Member save(Member member) {
        em.persist(member);
        return member;
    }
}

 

 

  •   컴포넌트 스캔 방법3 ( AutoConfig 설정 방식 )
@Configuration
@ComponentScan(
        excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
                Configuration.class))
public class AutoSpringConfig {

}

@ComponentScan 은 @SpringBootApplication 에 기본적으로 포함되어 있습니다.

그런데 만약 기존에 @Configuration 으로 컨테이너를 만들어 놨다면 두 설정파일을 읽어서 오류가 발생하겠죠??

 

이렇게 직접 컴포넌트 스캔을 설정해주면은 기존의 IoC컨테이너에 영향을 주지 않고

컴포넌트 스캔을 구현할 수 있습니다.

 

만약, 다른 IoC컨테이너가 없다면 따로 AutoConfig는 따로 설정할 필요 없습니다.

 

@SpringBootApplication 에 기본적으로 Component Scan 이 포함되어 있다는 의미는
IoC컨테이너가 없이도 @Component 관련 어노테이션을 다 읽는다는 의미이므로

@Repository 로 빈 등록하고,
@Configuration 내부에서 @Bean으로 또 등록하는 실수를 하게되면

Unique~~~ 와 관련 에러가 발생하니 유의하시길 바랍니다.

< 관련 오류 >
https://tjdwns4537.tistory.com/67?category=949574

 

 

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'JPQLMemberRepository' defined in file [/Repository/JPQLMemberRepository.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'javax.persistence.EntityManager' available: expected single matching bean but found 2: getEm,org.springframework.orm.jpa.SharedEntityManagerCreator#0

해결 과정.

1) IoC컨테이너에서 EntityManagerFactory, EntityManager를 따로 빈 등록하는 것이 아닌 하나의 함수에 묶어서 빈 등록

2) 레포지토리에서는 엔티티 매니저만 주입을 받음

3) 테스트코드에서는 레포지토리만 주입 받음

 

 

 

위의 과정 중 발생한 오류

 

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'xik.ShoppingMall.Repository.JPQLMemberRepositoryTest': Unsatisfied dependency expressed through field 'repository'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'xik.ShoppingMall.Repository.MemberRepository' available: expected single matching bean but found 2: JPQLMemberRepository,memberRepository

 

 

 

오류 키워드

1) NoUniqueBeanDefinitionException

2) available: expected single matching bean but found 2: JPQLMemberRepository,memberRepository

 

해당 키워드를 생각해보면 MemberRepository 두 개가 빈 등록 되어있어서 발생하는 오류임을 알 수 있습니다.

 

 

해결방법

: 기존에는 IoC컨테이너에서도 레포지토리를 빈 등록해주고, 레포지토리 클래스에서 @Repository를 해서 두번 빈 등록 해주고 있었습니다.

  - JPQLMemberRepository 에서 @Repository 할 꺼면 컨테이너에 레포지토리에 대한 빈 등록은 필요가 없습니다.

  - IoC컨테이너를 쓰려면 @Repository는 필요가없습니다.

  - 만약 두 군대에 레포지토리 등록을 사용하고 싶다면 JPQLMemberRepository 에 @Primary 를 붙쳐주면 해결됩니다.

 

 

* 빈 등록에 대해 숙지가 제대로 안된 상태라서 발생한 문제였습니다.

  빈 등록에 대해서 아래 주소에 블로깅해놨습니다.

 

https://tjdwns4537.tistory.com/70

 

Bean 등록 방법

 

tjdwns4537.tistory.com

 

 

 

 

 

 

저 같은 경우는 Entity 위치를 못 찾고 있어서 발생한 문제였습니다.

 

gradle 환경의 스프링에서 JPA를 사용하는데에에 있어서 persistence.xml로 설정을 하니 엔티티 위치를 못잡고 있었습니다.

 

persistence.xml에 아래 코드로 경로를 설정해주면 됩니다.

<class>xik.ShoppingMall.Domain.클래스명</class>
여러 클라이언트가 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.

 

이는 아주 중요한 개념입니다.

 

왜 중요할까요 ??

 

공유값이 바뀌는 에러가 발생

코드로 예를 들어보겠습니다.

[ SingleTon class ]

public class StateFulService {

    private int price; // 상태 유지 필드

    public void order(String name, int price) {
        System.out.println("name:" + name + " price:" + price);
        this.price = price;
    }

    public int getPrice() {
        return price;
    }
}

 

[ Test code ]

class StateFulServiceTest {
    @Test
    void FailSigleTon() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StateFulService stateFulService1 = ac.getBean("stateFulService", StateFulService.class);
        StateFulService stateFulService2 = ac.getBean("stateFulService", StateFulService.class);

        stateFulService1.order("parkA", 10000);
        stateFulService2.order("parkB", 20000);

        int price = stateFulService1.getPrice();

        System.out.println("price: " + price);

        Assertions.assertThat(stateFulService1.getPrice()).isEqualTo(20000);
    }

    static class TestConfig{

        @Bean
        public StateFulService stateFulService() {
            return new StateFulService();
        }
    }
}

위의 코드에서 보면 과정은 이렇습니다.

 

[ 테스트 코드에서 생성된 스프링 컨테이너는 기본적으로 싱글톤을 사용할 수 있게 설계되어 있습니다. ]

 

1)  parkA라는 주문을 후 getPrice함수로 바로 가격을 출력해줘야하는데

 

2) 그 중간에 parkB라는 주문이 들어옵니다.

 

3) 그러면 price 라는 객체를 공유하고 있기 때문에 10000 -> 20000으로 바꿔버리게 됩니다.

 

4) 그래서 stateFulService1 객체의 getPrice() 함수를 사용해도 20000이 출력되게 되는 오류입니다.

 

 

 

 

예시의 싱글톤이 이러한 에러가 발생하는 이유는 설계에 아주 큰 문제점이 있습니다.

 

*** " 테스트 코드 (클라이언트) 가 값을 변경한다 " 라는 것입니다. ***

 

그래서 스프링 빈은 항상 무상태로 설계해야 하는 것입니다.

 

그렇다면 위의 예시를 다시 무상태로 설계해보겠습니다.

 

 

[ SingleTon class ]

public class StateFulService {

    public int order(String name, int _price) {
        System.out.println("name:" + name + " price:" + _price);
        int price =  _price;
        return price;
    }
}

 

[ 테스트 코드 ]

class StateFulServiceTest {
    @Test
    void FailSigleTon() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StateFulService stateFulService1 = ac.getBean("stateFulService", StateFulService.class);
        StateFulService stateFulService2 = ac.getBean("stateFulService", StateFulService.class);

        int price1 = stateFulService1.order("parkA", 10000);
        int price2 = stateFulService2.order("parkB", 20000);

        Assertions.assertThat(price1).isEqualTo(10000);
    }

    static class TestConfig{

        @Bean
        public StateFulService stateFulService() {
            return new StateFulService();
        }
    }
}

 

이렇게 공유된 필드를 사용하는게 아니라 지역변수를 통해서 공유하지 못하게 해결할 수 있습니다.

 

 

이 외에 싱글톤의 문제점을 해결하는 방법으로 @Configuration 에노테이션을 사용하는 방법이 있습니다.
이 부분은 검색해보시면 많은 자료를 찾아볼 수 있습니다.

 

 

 

 

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springConfig': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'xik.ShoppingMall.Repository.MemberRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

위와 같은 에러가 발생하였습니다.
위의 에러를 해석해보면 빈을 등록할 때 에러가 발생한것으로 보입니다.

 

 

[ 기존 코드 ]

//    @Autowired
//    MemberServiceInterface memberService;
//
//    @Autowired
//    OrderService orderService;
//
//    @Autowired
//    DiscountPolicy discountPolicy;

 

 

[ 새로 작성한 코드 ]

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
MemberServiceInterface memberService = applicationContext.getBean("memberService", MemberServiceInterface.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

 

 

클라이언트 코드에서 스프링 컨테이너를 사용하는 버젼으로 바꾸기 위해 사용하다가 에러가 발생했습니다.

 

뭐가 문제인지 에러 코드를 다시 읽어보겠습니다.

 

 

 

UnsatisfiedDependencyException : Error creating bean with name 'springConfig'

: springConfig 라는 이름을 찾고있네요 ??

근데 제 코드를 보면 applicationContext 객체를 불러올때도 SpringConfig를 사용해주고 있고, 

애초에 클래스명도 SpringConfig로 적어둔 상태입니다.

근데 확인을 해보니 애초에 맨 앞 철자가 소문자로 변경된 형태로 등록되기 때문에 상관없는 문제라고 하네요.

 

그러면 다음 에러코드를 읽어보겠습니다.

 

: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'xik.ShoppingMall.Repository.MemberRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

위의  코드를 읽어보니 Repository에서의 bean을 등록해줄 때 생기는 문제 같습니다.

코드의 구성도를 보면

 

[ SpringConfig ]

    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

 

[ SpringDataJpaRepository ]

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    @Override
    Optional<Member> findByName(String name);

}

 

의심가는 부분이 있네요

applicationContext 로 getBean으로 빈들을 가져오는데,

제 코드 같은 경우 SpringConfig 생성자에서 JpaRepository로 구현된 레포지토리를 불러오는데

이 부분도  "@Autowired 로 주입을 해줄게 아니라 applicationContext로 주입을 해줘야하는게 아닌가? " 라는 생각입니다.

 

그런데 이 의문에도 확신이 없습니다.

의존성 주입과 빈 등록에 대해 확실히 알고 나서 진행해야할 것 같네요

https://tjdwns4537.tistory.com/52

 

의존성 주입과 빈 등록

IoC 컨테이너에 빈으로 등록이 되어야 의존성 주입을 할 수 있습니다. 먼저, 여러가지 의존성 주입 방법이 있지만 @Autowired를 통해서 주입하는 것을 알아보겠습니다. @Autowired 필요한 의존 객체의

tjdwns4537.tistory.com

 

정리를 해봤으니 에러에 대해 다시 분석해보겠습니다.

applicationContext는 스프링 컨테이너이고,

@Autowired는 의존관계 주입을 해주는 것이네요.

 

그러면 스프링 컨테이너에서 빈이 등록될 때 문제가 발생되는 것으로 보입니다.

 

[ IoC컨테이너 ]

[ 테스트 코드 ]

 

구성도를 보면

1) ApplicationContext 로 SpringConfig 컨테이너에 대한 빈 팩토리 생성

2) 팩토리의 getBean메서드를 통해 memberService,orderService라는 빈을 가져옴

* 그런데 memberRepository에 대한 빈이 없다는 에러내용

 => 결국, JpaRepository에서 자동으로 빈을 만들어주는 것으로 믿고 있었지만 실제로는 안만들어지고 있는것으로 보임

 => 순수 자바 코드인 MemoryMeberRepository로 @Bean등록해주고 테스트하니 정상 작동

 

 

 

* 결론

: @Autowired할 스프링 빈 대상이 없어 발생하는 문제입니다.

원인은 JpaRepository에서 빈을 등록해주는 과정에서 문제가 있는 것으로 보입니다.

그런데 JpaRepository를 상속받으면 자동으로 빈을 등록해주는 것으로 배웠는데 정확한 이유는 아직 파악하지 못했습니다.

이유를 알게되는대로 다시 블로그에 업데이트하도록 하겠습니다.

 

 

 

 

 

[ 2022.07.12 확인된 해결방법1 ]
Jpa이 버젼 문제였던 것으로 확인되었습니다.
spring-data-jpa 2버전은 spring-core 5부터 호환이 된다고 합니다.
버젼을 낮춰서 실행해보시면 해결됩니다.


[ 2022.07.12 확인된 해결방법2 ]
멤버 이름에 대소문자 구분 처리에 대한 에러가 있을 수 있다.

* 참고 : 아직 저는 해결되지 않았습니다.. ( 2022.07.19 해결됬습니다.. )

 

 

 

 

[ 해결완료 ]

해결방법 :

1) @Repository 어노테이션을 붙치면 레포지토리에 대한 에러가 @Component랑은 다르게 나온다는걸 알 게 되었습니다.

2) JpaRepository 상속받는 부분에 @Repository를 붙쳐주니 아래와 같은 에러로그를 볼 수 있습니다.

Repository인터페이스의 findall 메서드를 가리키고 있는거 같습니다. 그러면 서비스에서 findall을 호출해주는 부분을 찾아보겠습니다.

레포지토리의 findall을 실제 호출해주는 부분이 있습니다. 

 

그런데 기본적으로 jpaRepository를 상속받게 되면 기본적으로 제공되는 다양한 메서드들이 있습니다.

그 중에 findAll() 이라는 메서드가 있습니다.

그런데 저의 소스코드에서는 findAll과 같은 역할을 하는 findall 메서드가 존재합니다.

즉, 이유는 간단했습니다.

 

1) 컨트롤러에서 서비스의 findMeber() 메서드를 호출

2) findMember메서드에서는 findall() 메서드를 호출

3) 하지만 findall() 메서드는 jpaRepository 메서드와는 엄밀히 다른 메서드이므로 jpaRepository 상속 메서드 안에서 사용하고 싶다면

  따로 선언했어야함

   * JpaRepository 명명 규칙 : https://www.devkuma.com/docs/spring/jparepository%EC%9D%98-%EB%A9%94%EC%86%8C%EB%93%9C-%EB%AA%85%EB%AA%85-%EA%B7%9C%EC%B9%99/

 

4) MemberRepository에 선언되어 있는 findall() 메서드의 이름을 findAll() 로 바꿔준 후 서비스에서 사용

5) 테스트 결과 이상 없음

 

 

몇일 동안 이 문제로 삽질했는데 드디어 해결됬습니다 !!

IoC 컨테이너에 빈으로 등록이 되어야 의존성 주입을 할 수 있습니다.

먼저, 여러가지 의존성 주입 방법이 있지만 @Autowired를 통해서 주입하는 것을 알아보겠습니다.

 

@Autowired

필요한 의존 객체의 타입에 해당하는 빈을 찾아 주입합니다. 즉, IoC컨테이너 안에 등록되어 있는 빈에서 객체를 찾아 의존성 주입을 해줍니다.

 

 

Bean

스프링 IoC컨테이너가 관리하는 객체이다.

 

  •  장점

1) 의존성 관리가 용이

2) 빈으로 등록된 객체는 기본적으로 스코프가 "싱글톤"으로 정해짐

3) 라이프사이클 인터페이스를 지원해준다.

 

 

  •  BeanFactory

스프링 IoC컨테이너의 가장 최상위 인터페이스이다.

 

 

  • ApplicationContext

BeanFactory를 포함한 다른 컨테이너를 상속받는다.

 

 

  •   Bean 설정 방법 1

1) spring-boot-starter-web 의존성 설정

2) @Component 어노테이션을 클래스에 붙쳐준다. ( 자동으로 빈이 등록됨 )

3) @Autowired 어노테이션을 통해 의존성 주입을 해준다.

 

  • Bean 설정 방법 2 ( Java Config )

1) Config 파일에 @Configuration 어노테이션을 붙쳐준다.

2) @Bean을 통해 직접 빈을 등록해준다.

3) @Autowired를 통해 등록되어 있는 

 

 

 

 

정리

예제 코드를 보면

- @Bean은 Spring에 BookingService를 제공 ( 등록 )

- @Autowired는 이를 사용 ( 의존성 주입 )

 

@SpringBootApplication
public class Application {

  @Autowired
  BookingService bookingService;

  @Bean
  BookingService bookingService() {
    return new BookingService();
  }

  public static void main(String[] args) {
    bookingService.book("Alice", "Bob", "Carol");
  }
}
에러 코드 :
org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter [java.lang.Long arg0] in method [void xik.ShoppingMall.Service.OrderServiceImpTest.createOrder(java.lang.Long,java.lang.String,int)].

테스트 코드에서 의존성 주입을 하니 다음과 같은 에러가 발생하였습니다.

 

혹시나 싶어 순수 자바로 테스트를 해보려고 작성하다보니 이상한게 보이네요...

 

 

쓰지도 않는 인자들이 왜 들어있는지.. 지우니까 정상작동하네요..

 

허무한 해결완료입니다..

 

 

ㅇ GET/POST 이해하기

 

1) GET 란?

 - URL에 데이터를 포함시켜 요청

 - 데이터를 헤더에 포함하여 전송

 - URL에 데이터가 노출되어 보안에 취약

 - 캐싱할 수 있음

 => 주로 조회할때만 사용

 

2) POST 란?

 - URL에 데이터를 노출하지 않고 요청

 - 데이터를 바디에 포함

 - URL에 데이터가 노출되지 않아 GET방식보다 보안이 높음

 - 캐싱할 수 없음

=> 주로 노출되면 안되는 데이터를 저장할 때 사용

 

 

HttpMethod

: 다음의 각 메소드들은 HttpMethod에 매칭됩니다.

1) PostMapping

2) GetMapping

3) PutMapping

4) DeleteMapping

5) PatchMapping

 

 

 

ㅇ RequestMapping

1) 스프링부트 애플리케이션이 실행되면 애플리케이션에서 사용할 bean들을 담을 ApplicationContext를 생성하고 초기화합니다.

2) @RequestMapping 이 붙은 메서드들이 핸들러에 등록되는 것은 Application refresh되는 과정에서 일어납니다.

   refresh과정에서 Spring Application 구동을 위해 많은 Bean들이 생성되고,

  그 중 하나가 RequestMappingHandlerMapping 입니다. 이 Bean은 우리가 @RequestMapping 으로 등록한 메서드들을 가지고      있다가 요청이 들어오면 Mapping해주는 역할을 합니다.

3) Bean으로 등록된 HandlerMapping이 변수들을 찾아서 Adapter를 거쳐 실행합니다.

 

 

 

ㅇ Handler

: Spring MVC에서는 핸들러가 @Controller클래스를 의미합니다.

@GetMapping / @PostMapping 을 핸들러 메서드라고 부릅니다.

결국 Handler Mapping 이란 사용자의 요청과 이 요청을 처리하는 Handler를 매핑해주는 역할을 하는 것입니다.

 

 

 

 

ㅇ HandlerAdapter

:  스프링 부트가 아닌 다른 프레임워크의 핸들러를 Spring MVC에 통합하기 위해서는

 HandlerAdapter를 사용할 수 있습니다.

 

 

 

 

 ㅇ @RequestMapping

 

1) value 

: URL값으로 매핑 조건을 부여합니다. 보통 호스트주소와 포트번호를 제외한 url주소를 넣어줍니다.

  이는 다중 요청이 가능하여 @RequestMapping ( {"/hello". "/hello-rule", "/hello/**" } ) 형식으로 사용할 수 있습니다.

 

2) method

: HTTP request 메소드 값을 매핑조건으로 부여하는데 GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE 메소드가 존재합니다.

 

 

 

 

ㅇ PostMapping / GetMapping

: Post/Get method 로 RequestMapping을 합니다.

 

1) @PostMapping : HTTP Post Method에 해당하는 단축 표현으로 서버에 리소스를 등록(저장)할 때 사용합니다.

2) @GetMapping : HTTP Get Method에 해당하는 단축 표현으로 서버의 리소스를 조회할 때 사용합니다.

3) @DeleteMapping : 서버의 리소스를 삭제

4) @PutMapping : 서버의 리소스를 모두 수정

5) @PatchMapping : 서버의 리소스를 일부 수정

 

 

 

 

ㅇ 현재 저의 소스코드로 설명

@GetMapping("/new")
public String New() {
        return "/Login/회원가입";
}

@PostMapping("/new")
public String create(MemberForm form) {
        Member member = new Member();
        member.setName(form.getName());
        member.setPhoneNumber(form.getPhoneNumber());

        memberService.join(member);

        return "redirect:/";
}
<form action="/new" method="post">

 - 위의 소스 코드에 대한 설명 및 순서도

 

 

 

 

 

 

 

 

* 출처

https://itvillage.tistory.com/33

https://velog.io/@dyunge_100/Spring-%EC%9A%94%EC%B2%AD-%EB%B0%A9%EC%8B%9DRequestMapping-GetMapping-PostMapping

 

 

김영한님의 수업에서 JPA 실습을 해보던 중 아래와 같은 에러가 발생하였습니다.

javax.persistence.PersistenceException: org.hibernate.exception.SQLGrammarException: could not prepare statement

 

위의 에러 내용을 보면 Column "MEMBER0_.USER_NAME" not found; SQL statement: 라는 문구를 볼 수 있습니다.

 

그래서 몇가지 의심되는 부분에 대해 해결해봤습니다.

 

1. Member class 에 Column을 잘못 설정했는지 확인

: 테이블에 컬럼명을 이미 만들어 둔 상태기때문에 @Column으로 이름을 정해줘서 사용하는데, 이때 오타가 있나 확인해봤습니다.

 

 => 정상인 것으로 확인

 

2. application.properties 에 jpa 설정 제대로 추가해줬는지 확인 ( 해결 )    --> 아래 내용 읽어보기

spring.jpa.hibernate.ddl-auto = create

원래 create가 아닌 none으로 설정해줬었는데 이 부분에서 오류가 나고 있었습니다.

원래 제가 이해한 바로는 create는 객체에 대한 테이블과 테이블의 컬럼명을 자동으로 생성해주기때문에 저는 none을 사용했습니다.

그런데 이 부분에 대해 오류가 나고 있었던걸 보아 다시 개념을 확실히 잡고 넘어가야 할것 같습니다.

https://tjdwns4537.tistory.com/manage/newpost/?type=post&returnURL=%2Fmanage%2Fposts%2F 

 

TISTORY

나를 표현하는 블로그를 만들어보세요.

www.tistory.com

 

여기서 또 다시 드는 의문점은

create를 해주고 다시 none을 해준 후 테스트를 해보면 되는 것입니다.

그래서 테이블을 직접 확인해보니 phonenumber이였던 컬럼명이 phone_number이 되어있었습니다.

그래서 테이블을 삭제 후 다시 테스트를 해보던 중 아래와 같은 사실을 알게 되었습니다.

 

Member class에 각 객체에 @Column 으로 이름을 설정해줬는데

이때 @Column(name="phoneNumber") 로 설정을 하니까 PHONE_NUMBER로 읽고 PHONE_NUMER 컬럼을 찾으려고하니

에러가 발생하고 있던것입니다.

그래서 @Column(name="phonenumber")라고 설정하니 ddl-auto = none을 하고도 에러발생하지 않았습니다.

spring.jpa.hibernate.ddl-auto = none

이 외에도 column명을 설정할때 전부 대문자로 해도 에러는 발생하지 않습니다.

단지 소문자뒤에 대문자가 오면 구분자로 (_) 가 들어간다는 것입니다.

 

그래서 다시 확인을 해보니 다음과 같은 규칙이 있었습니다.

 

 

ㅇ JPA의 기본 테이블 DDL Naming 전략

 기본적으로 JPA는 DDL을 진행할 때 Entity 이름과 해당 필드들을 lower_under_score 방식을 이용합니다.

 예를 들어, @Column(name="PARK") 를 하면 Table에는 park로 lowerCarmelCase 를 lower_snake_case로

 변환하여 테이블을 생성합니다.

 

 이는 중요한 문제입니다.

 그 이유는 MySQL 에서 쿼리를 날릴 때에 [ 문자의 대소문자를 확실히 구분하는 설정 ] 과 [ 그렇지 않은 설정 ] 이

 있기 때문에 만약 확실히 구분하는 설정일 경우에 에러가 발생합니다.

 

 

* 해결 방법

SpringPhysicalNamingStrategy를 상속받아 Custom하여 설정값으로 물려주는 방법입니다.

 

1) 상속받는다.

2) 오버라이딩하여 함수를 작성해준다.

public class UpperCaseNamingStrategy extends SpringPhysicalNamingStrategy {

    @Override
    protected Identifier getIdentifier(String name, boolean quoted, JdbcEnvironment jdbcEnvironment) {
        return new Identifier(name.toUpperCase(), quoted); // 파라미터로 넘어온 name값을 대문자화 시킵니다.
    }
}

3) UpperCaseNamingStrategy 클래스를 application.yml에 설정해줍니다.

spring:
  jpa:
    hibernate:
      naming:
        physical-strategy: web.common.strategy.UpperCaseNamingStrategy # 커스텀클래스 패키지

 ==> 확인해보면 UPPER_SNAKE_CASE가 적용된 모습을 볼 수 있습니다.

 

 

 

 

- 결론

1) 애초에 컬럼명을 설정할 때, 대문자나 소문자 하나로 설정한다.

2) UpperCaseNamingStrategy 클래스를 이용한다.

 

 

 

 

* 링크

https://velog.io/@devduhan/Spring%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-JPA-Naming-%EC%A0%84%EB%9E%B5

 

# JPA설정 이해하기

spring.jpa.hibernate.ddl-auto = create

 

- 역할

 1) create : 해당하는 테이블이 있으면 DROP하고 새로 만들어버린다.

 2) create-drop : create와 같은데 종료시점에 테이블 DROP한다.

 3) update : 변경분만 반영

 4) validate : 엔티티와 테이블이 정상매핑되었는지만 확인

 5) none : 사용하지 않음 ( 사실상 없는 값이지만 관례상으로 적는다. )

 

- 주의사항

: create, create-drop, update는 로컬 또는 개발 초기단계 환경에서만 사용해야한다.

   위의 역할에서 처럼 테이블을 자동으로 DROP해버린다는건.. 상상도 하기 싫은 일이다.

 

인프런 김영한 강사님의 수업을 들으면서 실습하던 중 아래와 같은 에러가 발생하였습니다.

org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "INSERT INTO MEMBER(NAME) VALUE[*]()"; expected "DIRECT, SORTED, DEFAULT, VALUES, SET, (, WITH, SELECT, TABLE, VALUES"; SQL statement:

insert into member(name) value() [42001-200]



java.lang.IllegalStateException: org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "INSERT INTO MEMBER(NAME) VALUE[*]()"; expected "DIRECT, SORTED, DEFAULT, VALUES, SET, (, WITH, SELECT, TABLE, VALUES"; SQL statement:
insert into member(name) value() [42001-200]



Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "INSERT INTO MEMBER(NAME) VALUE[*]()"; expected "DIRECT, SORTED, DEFAULT, VALUES, SET, (, WITH, SELECT, TABLE, VALUES"; SQL statement:
insert into member(name) value() [42001-200]



2022-07-05 17:38:53.021  INFO 96188 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
MemberServiceIntegrationTest > join() FAILED

위와 같은 에러가 발생하는 이유는 강사님은 Member.name 에 대해서만 실습을 진행하였는데

저는 PhoneNumber 라는 변수 하나를 추가해서 실습했기 때문이라고 추정됩니다.

 

그런데 DB를 모르니 뭐가 문젠지 통 알 수가 없네요..

천천히 하나씩 뜯어보도록 하겠습니다.

 

가장 먼저

1) Syntax error in SQL statement "INSERT INTO MEMBER(NAME) VALUE[*]()"

2) insert into member(name) value() [42001-200]

 

이 두 문장을 봤을때 INSERT 쿼리에서 문제가 발생했다는 것 같습니다.

그래서 코드를 뜯어 보겠습니다.

"insert into member(name) value()";

- 해석 : member 테이블에 name 컬럼에 value() 값을 넣어준다.

이 부분에서 에러가 발생하는것 같습니다.. 그렇다면 h2 database 쪽 문제로 보이는데

 

 

1. dependency 버전 문제인가?

 

아래 사진을 보면 인텔리제이의 h2 버전이 제가 실행중인 h2 버전과 다른걸 알 수 있습니다.

 

 

제가 사용중인 버전은 아래 사진처럼 1.4.200입니다.

 

아래 사진과 같이 버전을 직접 적어서 dependency를 추가해줍니다.

 

똑같은 오류가 발생중입니다..

 

2. 예외의 원인을 찾아보자

IllegalStateException

위와 같은 예외가 제일 먼저 발생하는데 그 이유는 무엇일까?

 - 위와 같은 오류가 발생한 이유는 이전에 value 값이 들어가지 않았기 때문에 없는 value 에 접근하려고하니 에러가 발생하는 것이였다.

 

3. 오타 해결...

String sql = "insert into member(name) values(?)";

 1) value 가 아니라 values로 값 목록을 찾아야한다.

 2) values ( ? ) 은 레코드를 다수 넣기위한 하나의 문법이다.

 - 둘 이상의 값을 포함하는 배열을 생성 후

 - 쿼리문에 물음표를 작성하면 해당 자리가 값 배열로 대체됩니다.

String sql = "insert into member(name) values(?)";
PreparedStatement pstmt = null;
Connection conn = null;
pstmt = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);

 * Statement.RETURN_GENERATED_KEYS

 : DB상에 AUTO_INCREMENT로 인해 자동으로 생성되어진 key(=id) 를 가져오는 쿼리

 * Statement class

 : SQL 구문 실행하는 역할

 * PreparedStatement class

 : Statement를 상속받는 인터페이스로 동일한 SQL구문을 반복 실행한다면 향상된 기능으로 실행시키는 객체

 - 예시)

 // Statement class
String name = "홍길동";
String memo = "메모 테스트 입니다. 홍길동's 메모장";
String priority = "1";
String sql = String.format("insert into tblMemo values(memoSeq.nextval,'%s','%s',default,%s)", name, memo, priority); 


// PreparedStatement class
sql = "insert into tblMemo values(memoSeq.nextval,?,?,default,?)";
pstat = conn.prepareStatement(sql);

//매개변수 값 대입 + 매개변수 유효화 처리.
pstat.setString(1, name);
pstat.setString(2, memo);
pstat.setString(3, priority);

INFO: 0 containers and 3 tests were Method or class mismatch 아 같은 에러가 발생하였습니다.

 

이 에러는 테스트 메서드를 3개를 작성해놓고 1개만 실행시킨 경우에 발생하는 에러로 무시하고 진행해도 됩니다.

 

 

 

 

해결하고 싶은경우 맥OS 의 인텔리제이 기준으로

 

preference -> Build, Execution, Deployment -> Build Tools -> Gradle 에서

 

Build and run using Run tests using의 속성을 Intellij IDEA로 해주시면 해결됩니다.

 

스프링 부트 2.4에서 패치가 생겼습니다.

그래서 스프링 부트 2.4부터는 application.properties밑에 `spring.datasource.username=sa`를 꼭 붙쳐줘야합니다.

+ Recent posts