반응형

이 글에서는 페이스북 OAuth2.0 소셜 로그인 기능을 구현하는 방법에 대해서 기록해보고자 합니다.

 

이 글에서 사용한 프로젝트 스택은 Springboot 2.6.6 버전, Maven 빌드를 사용했습니다. 기본 로그인은 Spring Security를 사용하여 구현해 놓은 상태입니다.

 

OAuth2.0 클라이언트 라이브러리 추가하기

OAuth2.0 기능을 사용할 수 있도록 스프링에서 제공하는 OAuth2.0 클라이언트 라이브러리를 pom.xml에 추가해줍니다.

<!-- OAuth2 클라이언트 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

 

페이스북 로그인 구현하기

# 페이스북 개발자 센터 가입하기

저는 페이스북을 사용하지 않는데 소셜 로그인 기능 구현을 위해서 회원가입을 진행했습니다. 허허

https://developers.facebook.com/?locale=ko_KR 

 

Meta for Developers

꿈의 아틀리에 창조 BUCK의 크리에이터와 개발자로부터 Meta Spark를 사용하여 DIOR Beauty를 위한 AR 경험을 설계 및 빌드하는 과정에 대한 비하인드 스토리를 들어보세요. 이제 고급 액세스에 대한 비

developers.facebook.com

먼저 개발자 센터에 들어가서 본인의 페이스북 아이디로 로그인을 해줍니다.

 

 

# 개발자 센터 설정하기

My Apps > 앱 만들기에 들어가서 본인의 앱을 하나 만들어줍니다.

앱이 생성되고 들어가 보면 왼쪽 사이드에서 Facebook 로그인이라는 메뉴가 있습니다. 설정이 아래와 같이 되어있는지 확인해 줍니다.

 

 

설정이 확인되었으면 이제 아래 빠른 시작을 눌러서 웹을 선택하고 본인 사이트 URL을 입력해 줍니다.

 

 

저는 로컬 환경에서 진행할 것이기 때문에 http://localhost:8080으로 설정해 두겠습니다. 본인의 배포된 사이트가 있다면 그 주소를 입력해 주시면 됩니다. 저장을 누르고 계속하기를 하고, 다음도 계속 넘겨줍니다.

 

 

# 페이스북 로그인 버튼 추가하기

저는 JSP를 사용해서 구현했기 때문에 아래와 코드를 작성해 줬습니다.

코드는 그냥 기본 버튼을 사용해도 되고, 본인이 원하는 버튼을 사용해도 상관없습니다.

"/oauth2/authorization/facebook" 경로만 잘 설정해 주면 됩니다.

<!-- Facebook Oauth 소셜로그인 -->
<div class="login__facebook">
    <button onclick="javascript:location.href='/oauth2/authorization/facebook'" >
        <i class="fab fa-facebook-square"></i>
        <span>Facebook으로 로그인</span>
    </button>
</div>

<button>의 onclick 옵션에 javascript의 location.href 기능으로 바로 정해진 페이스북 OAuth2 로그인 요청 페이지로 이동하도록 설정해 줍니다. 

 

"/oauth2/authorization/facebook"은 내가 설정한 적도 없는데 저렇게 써도 되는 건가라는 궁금증이 있을 수도 있습니다. 우리가 제일 처음에 스프링이 제공하는 OAuth2.0 클라이언트 라이브러리를 추가해 준 이유입니다! yml을 설정하러 갑니다.

 

 

# application.yml OAuth2 설정하기

설정해줘야 하는 부분은 spring.security.oauth2.client.registration에 facebook에 대한 설정을 해줘야 합니다. 코드로 한번 살펴보겠습니다.

spring:
  security:
    oauth2:
      client:
        registration:
          facebook:
            client-id: {페이스북 개발자 센터의 앱 ID}
            client-secret: {페이스북 개발자 센터의 앱 시크릿 코드}
            scope:
              - public_profile
              - email

페이스북 개발자 센터의 앱 ID와 시크릿 코드는 개발자 센터에 들어가서 설정의 기본 설정으로 들어가면 확인하실 수 있습니다.

 

앱 ID와 앱 시크릿 코드

다음은 scope에 대해서 궁금하실 텐데 이 부분은 페이스북으로부터 사용자의 어떤 데이터를 받을지에 대한 범위를 지정할 수 있습니다.

 

개인정보는 민감한 부분이기 때문에 우리가 얻고 싶은 데이터를 얻는 것이 아니고 OAuth2.0를 제공하는 기업마다 다르기 때문에 문서를 참고해야 합니다. 우리는 email 정보와 프로필 정보를 가지고 있는 public_profile을 범위로 설정할 것입니다. 

 

이렇게 yml 설정을 마치면 앞서 jsp에서 작성했던 경로인 "/oauth2/authorization/{registration에 등록한 기업명}"을 이제 사용할 수 있게 됩니다.

 

그리고 추가적으로 이번에는 다루지 않을 내용이지만 카카오, 네이버 로그인 기능 구현에는 요구되는 사항이라서 얘기하고 넘어가겠습니다. 페이스북, 구글은 Spring OAuth2.0 Client에서 제공되는 provider에 대한 yml 설정을 따로 해주지 않아도 됩니다. 우리나라에서는 빈번하게 사용되는 카카오, 네이버의 경우에는 우리가 직접 provider로 등록해서 사용해야 합니다. 다음에 네이버 소셜 로그인 기능 구현에 대한 게시글을 정리할 때 자세히 적어보겠습니다.

 

 

# SecurityConfig 클래스 수정하기

이름은 설정하기 나름이겠지만 SpringSecurity를 사용하여 로그인 기능을 구현할 때 사용했던 WebSecurityConfigurerAdapter를 상속받은 클래스를 의미합니다. 저는 이름을 SecurityConfig라고 지정했습니다. 

 

아래는 기존의 SecurityConfig의 코드입니다.

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        
        http.authorizeRequests()
                .antMatchers("/", "/user/**", "/image/**", "/subscribe/**", "/comment/**", "/api/**")
                .authenticated()
                .anyRequest()
                .permitAll()
                .and()
                .formLogin()
                .loginPage("/auth/signin")
                .loginProcessingUrl("/auth/signin")
                .defaultSuccessUrl("/");
    }
}

 

 

configure() 메소드에 설정된 대로 SpringSecurity가 작동합니다. 위 코드는 OAuth2.0 보다는 SpringSecurity에 가깝기 때문에 자세히 설명하진 않겠습니다.

 

간단하게 말하자면 ["/", "/user/**", "/image/**", "/subscribe/**", "/comment/**", "/api/**"]에 해당하는 URL 요청이 들어온다면 인증이 필요합니다. 그 외의 요청이 들어오면 모두 허용합니다. 

 

그리고 로그인 인증 요청이 들어오면 로그인 페이지(/auth/signin)로 이동하고, 로그인 처리는 SpringSecurity에 위임합니다. 로그인이 성공한다면 홈(/)으로 이동한다는 의미의 코드입니다. 여기에 이제 OAuth2 처리에 대한 부분도 추가해 줍니다.

@RequiredArgsConstructor // OAuth2DetailService를 DI 받기 위한 Lombok 어노테이션
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final OAuth2DetailService oAuth2DetailService; // DI를 받아올 Service
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable();

        http.authorizeRequests()
                .antMatchers("/", "/user/**", "/image/**", "/subscribe/**", "/comment/**", "/api/**")
                .authenticated()
                .anyRequest()
                .permitAll()
                .and()
                .formLogin()
                .loginPage("/auth/signin")
                .loginProcessingUrl("/auth/signin")
                .defaultSuccessUrl("/")
                .and()
                .oauth2Login() // OAuth2 로그인 요청이 들어오면 처리해준다.
                .userInfoEndpoint() // 요청이 끝나면 인증코드가 아닌 회원정보를 받겠다는 코드
                .userService(oAuth2DetailService); // 해당 로직을 실행시키고, 데이터를 받을 서비스를 지정한다.
    }
}

새롭게 추가된 부분에는 주석을 달아놨습니다. 먼저 의문이 드는 코드가 한 두 개가 아닙니다.  순서대로 확인해 봅시다.

OAuth2DetailService
: 아직 구현하지 않은 클래스입니다. OAuth2 요청이 왔을 때, 그 응답 데이터를 받아서 로직을 처리하는 역할을 합니다.

.oauth2Login()
: OAuth2 로그인 요청이 왔을 때 처리해 줍니다. 우리는 스프링에서 제공하는 OAuth2 Client 라이브러리를 사용하고 있기 때문에 이렇게 쉽게 처리가 가능하지만 원래는 인가 코드를 받아서 AccessToken을 요청한 후 돌려받은 AccessToken을 가지고 사용자 데이터를 요청해야 합니다. 

.userInfoEndpoint()
: 위 요청이 끝나면 인증코드가 아닌 회원정보를 받겠다는 코드입니다.

.userService(oAuth2DetailService)
: oAuth2DetailService에서 OAuth2에 대한 요청에 대한 데이터를 받아서 로직을 처리하도록 합니다. 이때 userService에 들어갈 수 있는 타입은 DefaultOAuth2UserService입니다.

 

 

# OAuth2DetailService 생성하기

위에서 말했듯이 OAuth2 요청이 왔을 때 그 응답 데이터를 받아서 로직을 처리하는 역할을 하는 클래스입니다.

타입은 DefaultOAuth2UserService여야 하기 때문에 상속을 받아서 구현합니다.

@Service
public class OAuth2DetailService extends DefaultOAuth2UserService {
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        return null;
    }
}

loadUser의 파라미터를 보면 OAuth2UserRequest 타입의 변수가 있는데, 여기에 OAuth2 로그인을 요청한 사용자의 정보가 담겨서 온다. System.out.println()으로 oAuth2User를 출력해 보면 OAuth2 요청 버튼을 눌렀을 때, 정상적으로 사용자 정보가 들어오는 것을 확인할 수 있다.

 

 

# OAuth2DetailService 로그인 로직 구현

이제 받은 페이스북 사용자 정보를 가지고 로그인 기능을 구현해 볼 것이다. 가장 먼저 우리가 받아야 하는 정보는 username, password, email, name이다. 

@RequiredArgsConstructor // 생성자로 DI를 받아오기 위한 Lombok 어노테이션
@Service
public class OAuth2DetailService extends DefaultOAuth2UserService {
   private final UserRepository userRepository; // 회원을 DB에 넣기위해 DI를 받아옴

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        
        Map<String, Object> userInfo = oAuth2User.getAttributes();
        
        // facebook 사용자임을 구분하기 위해서 아이디 앞에 facebook_을 붙임
        String username = "facebook_" + (String) userInfo.get("id");
        
        // 암호는 임의의 UUID를 만들어 인코딩을 진행
        String password = new BCryptPasswordEncoder().encode(UUID.randomUUID().toString());
        
        // 받아온 데이터에서 email과 name에 해당되는 내용을 저장
        String email = (String) userInfo.get("email");
        String name = (String) userInfo.get("name");
        
        User userEntity = userRepository.findByUsername(username);
        
        // OAuth2 요청할 때마다 계속 INSERT되는 것을 막기위한 로직, 기존 회원이 존재하면 회원가입 로직 X
        if (userEntity == null) {
            // OAuth2 요청을 한 사용자가 기존 회원이 아닐 때, 새로운 User를 생성
            User user = User.builder()
                    .username(username)
                    .password(password)
                    .email(email)
                    .name(name)
                    .role("ROLE_USER")
                    .build();

            // UserDetails 타입으로 반환해야 Session에 저장하고 할 수 있다.
            // 그래서 PrincipalUserDetail에서 OAuth2User를 상속받게하여 loadUser에서 사용할 수 있도록 해준다.
            return new PrincipalUserDetails(userRepository.save(user), oAuth2User.getAttributes());
        }
        
        return new PrincipalUserDetails(userEntity, oAuth2User.getAttributes());
    }
}

요청받은 사용자 데이터에는 attributes라는 필드가 있고, 그 안에 우리가 필요로 하는 데이터들이 들어있습니다. 그렇기 때문에 Map<> 타입으로 우리가 필요한 데이터를 저장해 줍니다. 이때 attribute를 가져오기 위해서 getAttributes() 메소드를 사용합니다.

 

Map<>의 value 타입은 Object이기 때문에 userInfo.get()을 통해서 가져온 데이터는 각각에 맞는 데이터 형태로 형 변환을 해줘야 합니다. 이때 (String)을 주로 사용했습니다. 

 

username, password, email, name을 attribute에서 가져와서 값을 저장한 뒤 user 데이터베이스에 넣기 위해서 UserRepositroy의 save() 메소드를 사용합니다. 

 

근데 그 아래 return 되는 PrincipalUserDetailssms 무엇인지 궁금할 것이다. PrincipalUserDetails는 SpringSecurity 로그인 기능을 구현하기 위해서 만들어놨던 클래스이다. SpringSecurity에서 세션 정보를 저장하기 위해서는 UserDetails 타입으로 반환되어야 하는데, 그래서 PrincipalUserDetails를 이용해서 반환을 해준 것이다.

 

하지만 한 가지 걸리는 점이 있다. 세션을 위해 UserDetails 타입으로 반환해 준 것은 좋다. 근데 loadUser() 메소드의 return 타입을 보면 OAuth2User 타입이다. 즉, UserDetails 타입이면서 OAuth2User 타입도 되어야 하기 때문에 PrincipalUserDetails 코드를 조금 수정해야 한다. 

// OAuth2User 타입으로 만들기 위해서 OAuth2User를 상속받음
@Data
public class PrincipalUserDetails implements UserDetails, OAuth2User {

    private User user;
    
    // OAuth2 사용자 데이터의 attribute를 저장하기 위한 변수
    private Map<String, Object> attributes;

    public PrincipalUserDetails(User user) {
        this.user = user;
    }

    // OAuth2 유저 구분을 위한 생성자 오버로딩
    public PrincipalUserDetails(User user, Map<String, Object> attributes) {
        this.user = user;
        this.attributes = attributes;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collectors = new ArrayList<>();

        collectors.add(() -> {
            return user.getRole();
        });

        return collectors;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

기존 코드가 아닌 부분에는 주석을 달아놨다. 이렇게 OAuth2User 인터페이스를 상속받게 되면 PrincipalUserDetails 클래스는 UserDetails 타입이면서 OAuth2User 타입으로 사용할 수 있게 된다.

 

 

# 로그인 버튼 눌러서 확인해 보기

모든 기능 구현이 끝났기 때문에 만들어둔 로그인 버튼을 눌러서 로그인이 정상적으로 되는지 확인해 보자.

 

구현해둔 로그인 버튼

 

버튼을 눌러보면 아래와 같은 페이스북 로그인 페이지로 넘어가게 된다.

 

아이디와 비밀번호를 입력해서 로그인을 진행해 보면 로그인이 완료되는 것을 확인할 수 있다.

 

각자 진행하고 있는 프로젝트의 구조가 다르기 때문에 기능적으로 필요한 부분들만 얻어가셨으면 좋을 거 같다. 다음에는 카카오, 네이버, 구글 로그인에 대해서도 한번 다뤄볼 생각이다.

반응형
반응형

최근 스프링 부트 학습을 하는데 맨날 gradle로 빌드를 하다가 최근에 빌드 툴을 maven으로 하고 프로젝트를 생성했다. 초기 프로젝트 세팅은 아래와 같이 진행했다.

 

https://start.spring.io/

 

필요할 거 같은 라이브러리를 모두 추가해줬다. 압축파일을 풀고 인텔리제이에서 간단한 RestController를 하나 작성하고, 프로젝트를 실행시켜봤다. 근데 계속 아래와 같은 로그인하라는 페이지가 랜더링되었다. 난 저런 페이지를 추가한적이 없는데?

 

프로젝트 실행 시 8080 포트 들어가면 제일 먼저 뜨는 화면

 

뭘 로그인하라는거지라는 생각으로 구글링을 해봤고, Spring Security 라이브러리를 의존성 추가해놨기 때문에 뜨는 화면이라고 한다. 근데 지금은 사용하지 않을거 같아서 maven 초기 빌드를 진행하고, Spring Security에 관련된 코드는 모두 주석 처리 해놨다. 근데도 계속 Please Sign in 페이지가 뜬다. 어떻게 해결할 수 있을까

 

 

🔎 (방법 1) 로그에 출력되는 password를 사용해서 로그인

프로젝트를 실행하고 인텔리제이 로그를 확인해보면 password가 하나 만들어지는 것을 확인할 수 있다. Username에 user를 입력하고, Password에 로그에 출력되어있는 비밀번호를 입력해서 로그인하면 해결된다. 

 

근데, 프로젝트를 종료하고 다시 실행하면 계속 같은 화면이 뜬다. 계속 이렇게 로그인하는게 너무 귀찮아서 다른 방법이 있나 하고 계속 찾아봤다. 코드 상에서 Spring Security 기능을 비활성화해두는 코드를 하나 찾았다.

 

 

🔎 (방법 2) Spring Security 기능 비활성화 해두기

// Application.java

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class BlogApplication {
	public static void main(String[] args) {
		SpringApplication.run(BlogApplication.class, args);
	}

}

main 메서드가 존재하는 클래스에 들어가서 @SpringBootApplication에 위와 같이 exclude 값을 넣어주면 된다. Spring Security를 사용하지 않을 때, 비활성화 해둘 수 있는 코드라고 한다.

 

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)

이렇게 설정해주고 다시 프로젝트를 실행시켜보니 이제 Please Sign in 페이지가 안 뜨는 것을 확인할 수 있다. 후 속이 편안해졌다. 계속 로그인하느냐고 힘들었는데...

반응형
반응형

최근에 토비의 스프링 실습을 할 때 스프링 서버로 8080 서버를 사용했었다. 토비의 스프링 서버를 껐으니 당연히 포트도 닫혔겠지 했는데 웹 브라우저로 들어가보니 포트는 계속 열려있었다.

 

다른 스프링 학습을 하고 있는 도중에 아래와 같은 오류가 발생했다.

Web server failed to start. Port 8080 was already in use.

너무 직관적인 오류 내용이다. 웹 서버를 시작하는데 실패했다. 8080 포트가 이미 사용하고 있다. 음.. 인텔리제이도 모두 종료해보고 노트북도 재시동해봤지만 포트는 그대로 계속 열려있었다. 

 

1. Mac 터미널에서 문제를 해결하는 방법

# 명령어 입력

lsof -i tcp:8080

8080 포트를 사용하고 있는 프로세스를 확인

위 명령어를 입력하면 현재 8080 포트를 사용중인 프로세스를 확인할 수 있다. 다 껐다고 생각했는데 포트를 사용하고 있었다니...당연히 자동 종료하면 포트도 자동으로 닫히는줄 알았는데 너무 오래 켜놔서 그랬던건가 모르겠지만 계속 켜져있었다.

 

 

# PID 넘버를 사용해서 해당 프로세스 종료하기

sudo kill -9 {종료하려는 PID}

이렇게 명령어를 입력하면 Password를 치라는 칸이 나온다. 필자의 경우 맥북 비밀번호를 입력하니 6901 PID 프로세스가 포트 8080에서 종료된 것을 확인할 수 있었다. 

 

정상적으로 삭제되었는지 확인해보려면 다시 lsof -i tcp:8080을 쳐보면 된다. 아무것도 뜨지 않는다면 성공이다!

 

 

 

2. application.yml 설정을 변경하여 다른 포트 사용하기

이 부분은 스프링에서 application.yml 설정 파일에 다른 8080 포트 이외에 다른 포트를 사용하도록 설정할 수 있다. 

// application.yml

server:
  port: 8000

server.port: 8000으로 설정 값을 주면 해당 스프링이 이제 8080 포트에서 열리는 것이 아니고 8000 포트에서 열리는 것을 확인할 수 있다. 설정을 해주고 한번 프로젝트를 실행시킨 후 위에서 확인했던 명령어(lsof -i tcp:8000)로 한번 확인해보자.

 

8000 포트가 사용되고 있는 것을 확인

 

 

🙆🏻‍♂️ 마치며

lsof는 list open files의 약자라고 한다. 시스템에서 열려있는 파일들의 목록을 확인할 수 있는 명령어라고 한다. 예전에 Python이랑 Django 했을 때 썼던 포트가 8000이었던 거 같은데, 스프링에서도 포트를 변경해서 사용할 수 있다는 걸 알게되니 뭔가 신기했다. 지금은 위에서 설정한 application.yml대로 8000 포트를 사용해서 스프링 실습을 진행하고 있다. 

 

반응형
반응형

직면한 문제

기존에 h2 데이터베이스를 썼을 때, 사용했던 설정에서는 data-h2.sql 파일이 mysql에는 적용되지 않음.

 

 

MySQL 연동하기

dependencies {
	implementation 'mysql:mysql-connector-java'
}

먼저 mysql에 대한 의존성을 추가해주고, build.gradle을 refresh해서 적용시켜줍니다.

 

 

해결 방법

mysql을 이용하기 위한 application.yml 설정을 먼저 해줍니다.

spring:
    datasource:
        url: jdbc:mysql://localhost:3306/{스키마 이름}?serverTimezone=UTC&characterEncoding=UTF-8
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: {사용자이름}
        password: {비밀번호}

여기까지 설정해주면 기존에 h2 데이터베이스에서 실행되던 모든 것들이 mysql에서 실행이 됩니다. 테이블 생성까지는 모두 적용이 됐지만, 기존에 있는 data-h2.sql에 있는 초기 데이터 적용을 위한 sql 문이 실행이 되지 않았습니다.

INSERT INTO posts (created_date, modified_date, title, content, author) VALUES (now(), now(), '제목1', '본문1', '작성자1');
INSERT INTO posts (created_date, modified_date, title, content, author) VALUES (now(), now(), '제목2', '본문2', '작성자2');

위 코드는 data-h2.sql 파일 안에 있는 내용입니다. 이제 우리는 h2 데이터베이스가 아니고 mysql을 사용하기 때문에 파일이름을 먼저 data.sql로 바꿔줬습니다.

 

그 다음 아래와 같은 application.yml 설정을 추가해줬습니다.

  sql:
    init:
      mode: always

  jpa:
    defer-datasource-initialization: true
    hibernate:
      ddl-auto: create-drop

sql.init.mode.always는 모든 데이터베이스에 sql 스크립트를 적용하겠다는 설정입니다. 다음으로 jpa.defer-datasource-initialization.true는 h2 데이터베이스 게시글에도 설명했듯이 springboot 2.5 버전 이상부터는 data.sql 스크립트는 Hibernate가 초기화 되기 전에 실행된다고 한다. Hibernate가 초기화 된 후 sql 스크립트를 적용하기 위한 설정입니다.

 

data.sql에 있는 sql 스크립트가 서버를 돌렸을 때, 데이터가 초기화되는 것을 확인할 수 있습니다.

반응형
반응형

스프링부트로 웹 서비스 출시하기 1, 2, 3편을 보면서 실습하고 정리해둔 내용 입니다. 틀린 내용이 있다면 피드백 주시면 감사할 거 같습니다. 🙂

 

3번째 글까지 모두 완주했고, SpringBoot는 2.7.7 버전을 사용해서 진행했습니다. 자세한 소스코드는 깃허브 주소에 가시면 볼 수 있습니다.

 

📘 JPA/Javax 에서 제공하는 Annotation

@Entity

  • 데이터베이스 테이블과 연결되는 객체임을 나타내줍니다. 테이블의 이름은 ‘_’ 언더바를 이용하여 매칭합니다.
  • 예를 들어 파일이름이 MyPage.java라면 테이블 이름은 my_page로 매핑됩니다.

 

@Id

  • 해당 테이블의 primary key임을 나타냅니다.

 

@GeneratedValue

  • PK 생성 규칙을 결정할 수 있습니다. springboot 2.0 버전 이전에는 기본 값이 Auto로 설정되어 있지만 그 이후로는 직접 옵션을 추가해줘야 합니다.
  • 여기서 Auto란 데이터가 생성될 때마다 1씩 자동으로 증가하는 것을 의미합니다.
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    저는 springboot 2.7 버전을 사용하기 때문에 직접 IDENTITY 옵션을 추가해줬습니다.

 

@Column

  • Entity 객체 안에 정의되는 변수들은 모두 데이터베이스의 컬럼이 됩니다. Column 어노테이션을 쓰는 이유는 기본값 외에 옵션을 추가하고 싶을 때 사용합니다.
  • VARCHAR의 경우 기본값이 255인데 만약 500으로 늘리고 싶거나, TEXT 타입으로 컬럼을 사용하고 싶을 때 사용합니다.
    @Column(length = 500, nullable = false)
    private String title;
    
    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

 

@MappedSuperclass

  • 만약 JPA의 Entity 클래스가 MappedSuperclass로 선언된 추상 클래스를 상속받을 경우 createdTime와 modifiedTime도 Entity의 컬럼으로 인식하도록 합니다.
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public abstract class BaseTimeEntity {
        @CreatedDate
        private LocalDateTime createdTime;
    
        @LastModifiedDate
        private LocalDateTime modifiedTime;
    }

 

@EntityListeners(AuditingEntityListener.class)

  • 해당 클래스에 Auditing 기능을 포함시킵니다.

 

@CreatedDate

  • Entity가 생성될 때의 시간을 자동으로 저장합니다.

 

@LastModifiedDate

  • 조회한 Entity의 값을 변경했을 때의 시간을 저장합니다.

 

@EnableJpaAuditing

  • main 메서드가 있는 객체에서 Jpa의 Auditing 기능을 사용할 수 있게 해줍니다.
    @EnableJpaAuditing
    @SpringBootApplication
    public class SpringWebApplication {
    	public static void main(String[] args) {
    		SpringApplication.run(SpringWebApplication.class, args);
    	}
    }

 

@Transactional

  • Service 로직을 구성할 때 반드시 따라오는 어노테이션 중에 하나입니다.
  • 메서드 하나를 하나의 트랜잭션으로 하겠다는 것을 의미합니다. 여기서 하나의 트랜잭션이란 만약 save()라는 메서드에서 10개의 데이터를 저장해야 되는데, 6개를 저장하고 오류가 발생했다면 해당 데이터는 저장하지 않고 모두 rollback 시키는 것을 의미합니다.
    @Transactional
    public Long save(PostsSaveRequestDto dto){
        return postsRepository.save(dto.toEntity()).getId();
    }

 

 

📘 Lombok 라이브러리의 Annotation

 

@NoArgsConstructor

  • 기본 생성자를 자동으로 추가해줍니다.
  • 사용하는 이유는 프로젝트 코드 상에서는 Entity 객체를 기본 생성자로 생성하는 것을 막되, JPA에서 Entity 객체를 생성하는 것은 허용하기 위해서 추가해줍니다.
  • access = AccessLevel.PROTECTED 옵션을 추가해주면 기본 생성자가 protected로 생성됩니다.

 

@AllArgsConstructor

  • 해당 클래스 내에 있는 모든 필드를 인자 값으로 하는 생성자를 만들어줍니다.

 

@Getter

  • 클래스 내에 있는 모든 필드의 getter 메서드를 생성해줍니다.

 

@Setter

  • 클래스 내에 있는 모든 필드의 setter 메서드를 생성해줍니다.
  • Entity 객체에서는 setter는 잘 사용하지 않는다고 했지만, DTO와 같은 경우에는 setter를 사용합니다.
    • @RequestBody를 통해서 외부로 부터 데이터를 받는 경우에는 기본생성자 + setter를 통해서만 값이 할당되기 때문에 이때는 setter를 허용합니다.

 

@Builder

  • 해당 클래스의 빌더 패턴 클래스를 생성합니다.
  • 생성자 상단에 선언할 경우 생성자에 포함되어 있는 필드만 빌더에 포함됩니다.
    @Builder
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }

 

 

📘 Springboot/SpringFramework Annotation

 

@SpringBootTest

  • 통합 테스트를 기본으로 제공하는 어노테이션입니다.
  • Junit4를 사용할 때는 @RunWith(SpringRunner.class) 또는 @ExtendWith(SpringExtension.class과 같은 어노테이션을 추가해줘야 한다.
  • Junit5를 사용할 때는 별도로 추가해주지 않아도 된다.

 

@Controller

  • View를 반환하기 위해서 사용합니다. 여기서 말하는 View는 html과 같은 랜더링되는 파일을 의미합니다.
  • 만약 JSON 형태의 데이터를 반환하고 싶다면 @ResponseBody와 함께 사용해야 합니다.

 

@RestController

  • @Controller에 @ResponseBody를 더한 기능을 합니다. 즉, JSON 형태로 객체의 데이터를 반환합니다.
  • view를 반환하는 Controller와 JSON 형태의 데이터를 반환하는 RestController는 분리하여 사용하는 것이 좋습니다.

 

 

👨🏻‍💻 알아두면 좋을 지식들

1. 스프링 프레임워크에서 Bean을 주입(의존성 주입)받는 방식

  • 생성자를 통한 주입 (권장하는 방식)
    • 대부분의 의존 관계는 Application이 끝나기 전까지 변경될 일이 없기 때문에 생성자를 통한 의존성 주입 방법이 가장 권장되는 방법입니다.
    • 생성자를 자동으로 생성해주는 @AllArgsConstructor 또는 @RequiredArgsConstructor 어노테이션을 사용하면 생성자로 Bean을 주입받을 수 있다. 대신 Lombok을 사용해야하는 번거로움이 있지만, 코드 수정 시 필드가 추가되면 생성자에 일일히 코드를 추가시켜주는 번거로움을 없앨 수 있음.
    • Lombok을 사용하지 않는다면 생성자 위에 @Autowired 어노테이션을 선언하여 의존성을 주입할 수 있습니다. 만약 생성자가 하나밖에 없는 객체라면 굳이 @Autowired를 써주지 않아도 자동으로 주입이 됩니다.
  • setter 메서드를 통한 주입
    • setter 메서드 위에 @Autowired 어노테이션을 선언해주면 됩니다.
  • 필드(변수)를 통한 주입 (권장하지 않는 방식)
    • 필드에 직접 @Autowired를 선언하여 의존성을 주입하는 방법으로 실제 코드에서는 사용하지 않는 것이 좋습니다.
  • 일반 메서드를 통한 주입
    • 필드 여러 개에 동시에 의존성을 주입해줄 수 있지만 일반적으로 많이 사용되는 방법이 아닙니다.

 

2. DTO 객체를 따로 사용하는 이유

  • Entity 객체는 DB에 직관적으로 연결되어있는 객체이기 때문에 Entity를 건드리는 것은 위험이 있다.
  • 즉, DB를 위한 Layer와 View를 위한 Layer를 철저하게 구분하기 위헤서 DTO를 사용한다.
    • DB Layer는 Entity 객체에서 관리하고, View Layer는 DTO 객체에서 관리한다.

 

3. JPA Auditing을 사용하여 생성/수정 시간 자동화

  • 새로운 추상 클래스를 만들어서 Entity의 생성, 수정 시간을 자동적으로 저장할 수 있다.
  • LocalDateTime 클래스로 필드들을 선언하고, @MappedSuperclass와 @EntityListeners(AuditingEntityListener.class)를 사용하면 간단하게 기능을 생성할 수 있다.
반응형
반응형

🫤 문제 발생

메모리 데이터베이스인 H2에서 서버를 열고 닫을 때 마다 데이터가 없어지기 때문에 별도의 data-h2.sql 파일을 만들어 서버를 열 때마다 초기 데이터를 저장하도록 하려는 도중 모든 설정을 끝내고 확인해봤는데 데이터가 초기화되지 않는 문제가 발생

data-h2.sql

INSERT INTO posts (id, created_date, modified_date, title, content, author) VALUES (1, now(), now(), '제목1', '본문1', '작성자1');
INSERT INTO posts (id, created_date, modified_date, title, content, author) VALUES (2, now(), now(), '제목2', '본문2', '작성자2');

application.yml

spring:
  config:
    activate:
      on-profile:
        active: local

---
spring:
  profile: local

  jpa:
    open-in-view: false
    show-sql: true
    generate-ddl: true
    hibernate:
      ddl-auto: create-drop

  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
    data: classpath:data-h2.sql

  h2:
    console:
      enabled: true

  main:
    allow-circular-references: true

🔎 문제 해결 방안 찾기

1. 첫 번째로 찾았던 방법은 spring boot 2.5 버전 이상에서는 아래와 같은 옵션을 추가해줘야된다고 한다.

jpa.defer-datasource-initialization=true

Spring Boot 2.5버전 부터 스크립트 기반 초기화의 동작과정을 Flyway, Liquibase와 일치시키기 위해서 data.sql은 Hibernate 초기화되기 전에 실행된다고 한다.

즉, 생성된 스키마에 data.sql의 데이터를 초기화하고 싶다면 위에서 말한 jpa.defer-datasource-initialization=true에 대한 옵션을 추가해줘야 한다. 더 자세한 내용이 궁금하다면 참고했던 블로그인데, 참고하면 좋을 것 같다.

 

해당 옵션을 추가해주고난 후 실행해봤다. 근데 왜 작동이 안 되지…라는 생각을 하고 더 찾아봤다. 속 시원하게 나왔던 stackoverflow의 내용을 가져와봤다.

 

2. data.sql과 data-h2.sql의 차이점

내가 지금하고 있는 실습에서는 data.sql이 아닌 data-h2.sql을 사용하고 있다. 위 링크에 들어가서 확인해본 내용은 다음과 같다.

data-h2.sql 또는 data-mysql.sql의 경우에는 내가 설정한 database platform에 따라서 결정된다고 한다.

spring.sql.init.platform=h2 # Spring Boot >=v2.5.0
spring.datasource.platform=h2 # Spring Boot <v2.5.0

나는 2.7.7 버전을 사용하고 있기 때문에 spring.sql.init.platform=h2 옵션을 추가해줬다.

최종 application.yml 파일

jpa에 해당하는 부분만 수정이 있었기 때문에 그 부분만 확인해보시면 빠를 거 같습니다!

spring:
  config:
    activate:
      on-profile:
        active: local

---
spring:
  profile: local

  sql:
    init:
      platform: h2

  jpa:
    open-in-view: false
    defer-datasource-initialization: true
    show-sql: true
    generate-ddl: true
    hibernate:
      ddl-auto: create-drop

  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
    data: classpath:data-h2.sql

  h2:
    console:
      enabled: true

  main:
    allow-circular-references: true

정상적으로 돌아가는 것을 확인했다.

POSTS 테이블에 있는 모든 행을 가져오는 SQL 쿼리

실습 권장 버전은 1.5.0버전이었는데 젤 최근 버전에 맞춰서 해보고 싶어서 하고 있는데, 시간은 오래걸리는데 나름 배워가는 것이 더 많은거 같아서 재밌다. 😮

반응형

+ Recent posts