스프링부트와 AWS로 혼자 구현하는 웹서비스 따라하기


java, gradle, JPA 와 mustache, js 로 만든 우리의 웹 페이지의 CRUD 가 잘 작동하게 만들었다.

이제 만들어진 페이지에 로그인을 만들어볼 거다.
로그인 된 회원이 GUEST 냐 USER 냐 에 따라 게시글 작성가부를 결정하는 기능이다.

시작👊

우리는 구글 클라우드 플랫폼 서비스를 이용해볼 거다.
나중에 프로젝트를 완성한 뒤에 내 나름대로 카카오도 추가해볼 생각이다.


ㄱㄱ

구글 클라우드 등록하기

구글 클라우드 플랫폼을 이용해 웹 페이지에 로그인 기능 구현하기!

우선 https://console.cloud.google.com/ 에 들어간다.
구글 로그인을 한 뒤에

새 프로젝트 만들기

  1. 새 프로젝트 만들기를 클릭한다.
    프로젝트 이름을 적고
  1. API 및 서비스에서 OAuth 동의 화면 에 들어간다.

  2. 애플리케이션 이름과 사용자 지원 이메일을 채워넣는다.

애플리케이션 이름 : 구글 로그인 시 사용자에게 노출 될 애플리케이션 이름을 이야기합니다.
지원 이메일 : 보통은 서비스의 help 이메일 주소를 사용하지만, 일단 사용하는 이메일을 씁시다.

  1. API 범위는 email, profile, openid 를 적는다.

Google API 범위 : 이 프로젝트에서는 기본 범위만 사용합니다.

  1. 저장 후 계속, 저장 을 눌러 테스트사용자와 요약을 넘어가 동의를 완료한 다음에 사용자 인증정보 - OAuth 클라이언트 ID 를 누른다.

  2. 웹 애플리케이션을 클릭한다.

  3. 승인된 리디렉션 URI 에 주소를 넣는다

http://localhost:8080/login/oauth2/code/google

어 스크린샷에 오타가 있네
cod 가 아니라 code

승인된 리디렉션 URI : 서비스에서 파라미터로 인증 정보를 주었을 때 인증이 성공하면 구글에서 리다이렉트할 URL입니다. 스프링 부트 2 버전의 시큐리티에서는 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드} 를 지원합니다. 시큐리티에서 리다이렉트 URL을 지원하는 Controller 를 구현해 놓은 상태기 때문에 우리가 별도로 만들 필요가 없습니다.
현재는 개발 단계이므로 localhost 를 사용하고 AWS 에 배포할 때가 되면 localhost 외에 추가로 주소를 기입해야합니다.

코드를 적자!

경고⛔
모든 코드가 완성되기 전에 아직 없는 클래스를 적을 때도 있어서 컴파일에러가 발생하기도 하는데, 일단 저자를 믿고 적어넣자. 모든 클래스를 만들고 나면 자연스레 사라져있다.

이제 인텔리제이 차례

  1. application.properties 옆에 application-oauth.properties 를 만들어준다. 그리고 코드를 적어준다.
# application-oauth.properties

spring.security.oauth2.client.registration.google.client-id=클라이언트아이디
spring.security.oauth2.client.registration.google.client-secret=클라이언트보안비밀
spring.security.oauth2.client.registration.google.scope=profile,email

클라이언트 아이디에는 조금 전 구글 플랫폼 에서 만들어준 그 코드
클라이언트보안비밀에는 그 아래 코드를 적어준다.

scope=profile,email
scope 의 기본 값이 openid,profile,email 이기 때문에 많은 예제에서 이 scope 를 별도로 등록하고 있지 않은데, 우리가 profile,email 을 등록한 이유는 openid라는 scope 가 있으면 Open Id Provider 로 인식하기 때문입니다. 그렇게 되면 Openid Provider 인 서비스(구글)와 그렇지 않은 서비스(네이버/카카오 등)로 나눠서 각각 OAuth2Service 를 만들어야합니다.
그래서 하나의 OAuth2Service 로 사용하기 위해 일부러 openid scope 를 빼고 등록합니다.

정말 이사람은 천재일까. 어떻게 다 아는 거야?

스프링 부트에서는 properties의 이름으로 application-###.properties 로 만들면 ### 라는 이름의 profile 이 생성 되어 이를 통해 관리할 수 있습니다. 즉, profile=### 이라는 식으로 호출하면 해당 properties의 설정들을 가져올 수 있습니다. 호출하는 방식은 여러가지지만 이 책에서는 스프링부트의 기본 설정 파일인 application.properties에서 application-oauth.properties를 포함하도록 구성할 것입니다. 그래서

  1. application.properties에 코드를 추가합니다.
spring.profiles.include=oauth

이제 이 설정값을 사용할 수 있게 되었습니다.

와아!! 박수 🙌

.gitignore

우리가 적어넣은 클라이언트id 와 클라이언트보안비밀번호는 매우 민감한 것으로 만약 깃허브에 업로드하고 있다면 필히 appication-oauth.properties 파일을 제외하고 푸시해줘야한다. 그래서 .gitignore 에 적어줄거다

# .gitignore

application-oauth.properties

그리고 커밋할 때 oauth 파일이 목록에 나오지 않아야한다.
그런데 나는 나왔다. 그래서 저자의 블로그를 참고했다.

참고 : 저자 블로그

하라는 대로 커밋한 다음에 푸시까지해주면 된다.

oauth파일이 안 보인다!참 친절하셔.

User 만들기

User 클래스를 생성해야겠지.

  1. domain 아래에 user 패키지를 만들고 User 클래스를 만든다.
  2. 코드를 넣어준다.
// User.java

package com.prac.webservice.springboot.domain.user;

import com.prac.webservice.springboot.domain.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;

    @Column
    private String picture;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    @Builder
    public User(String name, String email, String picture, Role role){
        this.name = name;
        this.email = email;
        this.picture = picture;
        this.role = role;
    }

    public User update(String name, String picture){
        this.name = name;
        this.picture = picture;

        return this;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }
}

Enumerated(EnumType.STRING)
JPA 로 데이터베이스를 저장할 때 Enum 값을 어떤 형태로 저장할지를 결정합니다. 기본적으로 int 로 된 숫자가 저장되는데, 숫자로 저장되면 데이터베이스로 확인할 때 그 값이 무슨 코드를 의미하는 지 알 수가 없습니다. 그래서 STRING 으로 저장되도록 선언합니다.

오오... 참 세심하셔...

  1. User 옆에 사용자의 권한을 관리할 Enum 클래스 Role 을 생성합니다.
  2. 코드를 적어넣자
// Role.java

package com.prac.webservice.springboot.domain.user;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Role {

    GUEST("ROLE_GUEST", "손님"),
    USER("ROLE_USER", "일반 사용자");

    private final String key;
    private final String title;
}

이걸 처음에 이해못했다. enum 이 뭐지? 코드 형태도 처음보는 거였다. 사실 아직 이해 못 한다. 사용만 할 줄 알지.

ROLE_GUEST, ROLE_USER : 스프링 시큐리티에서는 권한 코드에 항상 ROLE_ 이 앞에 있어야 합니다.

  1. user 패키지 안에 UserRepository interface 를 만들어주자.
// UserRepository.java

package com.prac.webservice.springboot.domain.user;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmail(String email);
}

findByEmail : 소셜 로그인으로 반환되는 값 중 email 을 통해 이미 생성된 사용자인지 처음 가입하는 사용자인지 판단하기 위한 메서드 입니다.

User Entity 관련 코드는 끝!
이해한 건 하나도 없지만, 어쨋든 끝!
다 읽을 줄 아는 코든데 어떻게 작용하는지 하나도 모르는 내 뇌가 미스터리다 정말.

스프링 시큐리티 설정하기

  1. build.gradle 에 스프링 시큐리티 관련 의존성 하나를 추가합니다.
    implementation('org.springframework.boot:spring-boot-starter-oauth2-client')

소셜 로그인 등 클라이언트 입장에서 소셜 기능 구현 시 필요한 의존성입니다. spring-security-oauth2-clientspring-security-oauth2-jose 를 기본적으로 관리해줍니다.

그게 몬가요..ㅠ
책을 읽으면서 알아가야하는데 책을 읽다 몰라서 찾아야하는 게 더 많다.
처음 공부하는 사람들과 나처럼 눈은 처음이 아니라 하지만 뇌가 초면이라고 하는 사람들에겐 쉽지 않은 책인가보다 ㅠㅠ

이제 OAuth 라이브러리르 이용한 소셜로그인 설정 코드를 작성합니다.

  1. springboot 아래에 config.auth 라는 패키지를 만든다.

    시큐리티 관련 클래스는 모두 여기에 담는다고 보며 됩니다.

  2. SecurityConfig 클래스를 만들고 코드를 적는다.

// SecurityConfig.java

package com.prac.webservice.springboot.config.auth;

import com.prac.webservice.springboot.domain.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    private final CustomOAuth2UserService customOAuth2UserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .headers().frameOptions().disable()
                .and()
                    .authorizeRequests()
                    .antMatchers("/","/css/**","/images/**","/js/**","/h2-console/**").permitAll()
                    .antMatchers("/api/v1/**").hasRole(Role.USER.name())
                .anyRequest().authenticated()
                .and().logout().logoutSuccessUrl("/")
                .and().oauth2Login().userInfoEndpoint().userService(customOAuth2UserService);
    }
}

적으면서 진짜 하나도 이해 못한 코드다. 컴파일에러도 나고 난리났다. 하지만 자연스레 없어진다니 일단 적는다.

EnableWebSecurity : Spring Security 설정들을 활성화 시켜줍니다.
csrf().disable().headers().frameOptions().disable() : h2-console 화면을 사용하기 위해 해당 옵션들을 disable 합니다.
authorizeRequests : URL 별 권한 관리를 설정하는 옵션의 시작점. 이게 선언되어야만 antMatchers 옵션을 사용할 수 있습니다.
antMatchers : 권한 관리 대상을 지정하는 옵션입니다. URL, HTTP 메서드 별로 관리가 가능합니다. "/" 등 지정된 URL 들은 permitAll() 옵션을 통해 전체 열람 권한을 주었습니다. "/api/v1/**" 주소를 가진 API 는 USER 권한을 가진 사람만 가능하도록 했습니다.
anyRequest 설정된 값들 이외 나머지 URL 들을 나타냅니다. 여기서는 authenticated()를 추가하여 나머지 URL들은 모두 로그인된 사용자들에게만 허용하게 합니다.

? anyRequest 를 이해 못 함
authenticated()가 antMatchers 로 권한을 나눴는데 anyRequest 는 왜 또 로그인된 사용자들에게만 허용해? 설정된 값 이외라면 전체허용이어야하는 거 아닌가?
아니 권한 관리 대상을 authorize랑 ant 가 지정해서 로그인 여부에 따라 URL 접근을 허용하고 나머지는 다... 되야하는거아니야? authorize 가 그냥 ant 로 가는 거쳐가는 단계게 아닌가? 거쳐가는 단계가 아니라 더 큰 어떤 범주의 그런 건가?
😔
나중에 찾아봐야지..ㅠ

일단 ㄱ

logout().logoutSuccessUrl("/") : 로그아웃 기능에 대한 여러 설정의 진입점입니다. 로그아웃 성공시 "/" 주소로 이동
oauth2Login : OAuth 2 로그인 기능에 대한 여러 설정의 진입점
userInfoEndpoint : OAuth 2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당합니다.
userService : 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록합니다. 리소스 서버(즉, 소셜 서비스들) 에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시할 수 있습니다.

너무 어렵습니다 선생님. 갑자기 난이도가 올라갔어요.🥺



  1. config.auth 에 CustomOAuth2UserService.java 를 만들어 코드를 적는다

    이 클래스에서는 구글 로그인 이후 가져온 사용자의 정보를 기반으로 가입, 정보수정, 세션 저장 등의 기능을 지원합니다.

아하!

// CustomOAuth2UserService.java
package com.prac.webservice.springboot.config.auth;

import com.prac.webservice.springboot.config.auth.dto.OAuthAttributes;
import com.prac.webservice.springboot.config.auth.dto.SessionUser;
import com.prac.webservice.springboot.domain.user.User;
import com.prac.webservice.springboot.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpSession;
import java.util.Collections;

@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    private final UserRepository userRepository;
    private final HttpSession httpSession;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest)
            throws OAuth2AuthenticationException {

        OAuth2UserService delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        String registrationId =
                userRequest
                        .getClientRegistration()
                        .getRegistrationId();
        String userNameAttributeName =
                userRequest
                        .getClientRegistration()
                        .getProviderDetails()
                        .getUserInfoEndpoint()
                        .getUserNameAttributeName();

        OAuthAttributes attributes =
                OAuthAttributes
                        .of(
                                registrationId,
                                userNameAttributeName,
                                oAuth2User
                                        .getAttributes());

        User user = saveOrUpdate(attributes);

        httpSession.setAttribute("user", new SessionUser(user));

        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
                attributes.getAttributes(),
                attributes.getNameAttributeKey());
    }

    private User saveOrUpdate(OAuthAttributes attributes){
        User user = userRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(
                        attributes.getName(),
                        attributes.getPicture()))
                .orElse(attributes.toEntity());

        return userRepository.save(user);
    }


}

registrationId : 현재 로그인 진행중인 서비스를 구분하는 코드입니다. 현재 구글만 사용하고 있어 불필요한 부분이지만, 나중에 네이버 카카오를 연동하게 되면 네이버로그인인지 구글로그인인지 카카오로그인인지 구분하는 코드로 사용됩니다.
userNameAttributeName : OAuth2 로그인 진행 시 키가 되는 필드값을 이야기합니다. Primary Key 와 같은 의미입니다. 네이버 카카오등은 기본 지원하지 않습니다. 구글의 기본 코드는 "sub" 입니다. 네이버로그인과 구글 로그인을 동시 지원할 때 사용됩니다.
OAuthAttributes : OAuth2UserService 를 통해 가져온 OAuth2User 의 attribute 를 담을 클래스입니다. 이 후 네이버 등 다른 소셜 로그인도 이 클래스를 사용합니다.
SesstionUser 세션에 사용자 정보를 저장하기 위한 Dto 클래스입니다.

사용자 정보가 업데이트 되었을 때를 대비해 save 와 함께 update 도 구현되어있습니다. 사용자의 이름이나 프로필사진이 변경되면 User Entity 에도 반영됩니다.

  1. auth/dto 에 OAuthAttributes 클래스를 생성합니다.

    저자의 경우 OAuthAttributes 는 Dto 로 보기 때문에 dto에 넣었습니다.

// OAuthAttributes.java

package com.prac.webservice.springboot.config.auth.dto;

import com.prac.webservice.springboot.domain.user.Role;
import com.prac.webservice.springboot.domain.user.User;
import lombok.Builder;
import lombok.Getter;

import java.util.Map;

@Getter
public class OAuthAttributes {
    private Map<String, Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;
    private String picture;

    @Builder
    public OAuthAttributes(Map<String, Object> attributes,
                           String nameAttributeKey,
                           String name,
                           String email,
                           String picture){
        this.attributes = attributes;
        this.nameAttributeKey = nameAttributeKey;
        this.name = name;
        this.email = email;
        this.picture = picture;
    }

    public static OAuthAttributes of(String registrationId,
                                     String userNameAttributeName,
                                     Map<String, Object> attributes){
        return ofGoogle(userNameAttributeName, attributes);
    }

    private static OAuthAttributes ofGoogle(String userNameAttributeName,
                                            Map<String, Object> attributes){
        return OAuthAttributes.builder()
                .name((String) attributes.get("name"))
                .email((String) attributes.get("email"))
                .picture((String) attributes.get("picture"))
                .attributes(attributes)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }

    public User toEntity(){
        return User.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.GUEST)
                .build();
    }
}

of() : OAuth2User 에서 반환하는 사용자 정보는 Map 이기 때문에 값 하나하나를 변환해야만 합니다.
toEntity : User Entity 를 생성합니다. OAuthAttributes 에서 엔티티를 생성하는 시점은 처음 가입할 때입니다. 그 때의 기본 권한ㅇ르 GUEST 로 주기 위해서 role 빌더값에는 Role.GUEST 를 사용합니다.

  1. dto 패키지에 SessionUser 클래스를 추가한다.
//SessionUser.java

package com.prac.webservice.springboot.config.auth.dto;

import com.prac.webservice.springboot.domain.user.User;
import lombok.Getter;

import java.io.Serializable;

@Getter
public class SessionUser implements Serializable {
    private String name;
    private String email;
    private String picture;

    public SessionUser(User user){
        this.name = user.getName();
        this.email = user.getEmail();
        this.picture = user.getPicture();
    }
}

SessionUser 에는 인증된 사용자 정보만 필요합니다. 그외에는 필요하지 않으니 name, email, picture 만 필드로 선언합니다.

코드는 다 적었고 🤔 테스트 전에 궁금했던 점을 살펴보고 갑시다!

왜 User 클래스를 사용하면 안 되나요?

만약 User 클래스를 그대로 사용했다면

Failed to convert from type [java.lan.Object]
to type [byte[]] for value 'com.prac.webservice.springboot.domain.user.User@4a43d6'

이런 에러가 발생하게 됩니다.


이는 User 클래스를 세션에 저장하려고 하니 User 클래스에 직렬화가 되어있지 않아 발생하는 오류입니다. 그렇다고 User 클래스에 직렬화 코드를 넣으면 어떻게 될까요? User 클래스가 Entity 기 때문에 생각해야할 것이 많습니다. Entity 클래스는 언제 다른 Entity 와 연결될지 모릅니다. 예를 들어 @OneToMany @ManyToMany 등 자식 엔티티를 갖고 있다면 직렬화 대상에 자식들 까지 포함되니 성능이슈, 부수 효과가 발생할 가능성이 큽니다. 그래서 직렬화 기능을 가진 세션 Dto 를 하나 추가로 만드는 것이 이후 운영 및 유지보수 때 많은 도움이 됩니다.

선생님은 천재십니다. 프로젝트를 할 때 user 가 안 받아지길래 그냥 직렬화 해버렸을 때가 있다. 그 때 이걸 알았다면! 그 때 그 문제가 이 문제였는지 기억은 안나지만, 내 뇌야 화이팅!😆

테스트

아차! 로그인 버튼을 만들자

기능이 있어도 버튼이 없으면 로그인을 할 수가 없어요. 로그인 버튼을 만들어 봅시다.

  1. index.mustache 에 로그인 버튼과 로그인 성공시 사용자이름을 index 페이지에 걸어보자.
<!-- index.mustache -->
	...

    <!-- 로그인 영역 -->
    <div class="row">
        <div class="col-md-6">
            <a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
            {{#userName}}
                Logged in as: <span id="user">{{userName}}</span>
                <a href="/logout" class="btn btn-info active" role="button">Logout</a>
            {{/userName}}
            {{^userName}}
                <a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>
            {{/userName}}
        </div>
    </div>

	...

{{#userName}} : 머스테치는 다른 언어와 같은 if문(if userName != null 등) 을 제공하지 않습니다. 그저 true/false 만 판단하므로 머스테치에서는 항상 최종값을 넘겨줘야합니다. 그래서 userName 이 있다면 userName을 노출시키도록 구성했습니다.

오 무슨 뜻인지 전혀 몰랐는데 신기합니다. 그래서 # 안에 logout 버튼이 있구나.

a href="/logout" : 스프링 시큐리티에서 기본적으로 제공하는 로그아웃 url입니다. 즉, 개발자가 별도로 저 url 에 해당하는 컨트롤러를 만들 필요가 없습니다. SecurityConfig 클래스에서 url을 변경할 순 있지만 기본 url을 사용해도 충분하니 여기서는 그대로 사용합니다.
{{^userName}} : 머스테치에서 해당 값이 존재하지 않은 경우에는 ^ 를 사용합니다.

아하! 그래서 여기에는 Login 버튼이 있구나!

신기방기

a href="/oauth2/authorization/google" : 스프링 시큐리티에서 기본적으로 제공하는 로그인 url 입니다. 로그아웃 url 과 마찬가지로 개발자가 별도의 컨트롤러를 생성할 필요가 없습니다.

너무 좋은 스프링 시큐리티. 사랑합니다.👍

  1. userName 을 사용할 수 있도록 IndexController 에서 userName을 model에 저장하는 코드를 추가합니다.
// IndexController.java

public class IndexController {

    private final PostsService postsService;
    private final HttpSession httpSession;			// HttpSession 코드도 추가합시다.

    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute("posts", postsService.findAllDesc());
        SessionUser user = (SessionUser) httpSession.getAttribute("user");
        if (user != null){
            model.addAttribute("userName", user.getName());
        }
        return "index";
    }
    
	...

(SessionUser) httpSession.getAttribute("user") : CustomOAuth2UserService 에서 로그인 성공 시 세션에 SessionUser 를 저장하도록 구성했습니다. 즉, 로그인 성공 시 httpSession.getAttribute("user")에서 값을 가졍로 수 있습니다.
if(user != null) : 어려운 코드는 아니지만 세션에 저장된 값이 있을 때만 model 에 userName으로 등록합니다. 세션에 값이 없으면 model 엔 아무런 값이 없는 상태이니 로그인 버튼이 보입니다.

진짜 테스트

로그인 버튼을 눌러 로그인 해서 글을 등록해보자!

메인페이지에서 로그인 버튼을 눌러 내 아이디로 로그인하고, 글 등록을 누르면?

❌ ERROR

? 403?

몬데... 또 왜이러는데..

오랜만에 에러가 떠서 당황했지만, 다음 페이지의 저자는 당황하지 않았다. guest 신분이라 그런 거니 우리 아이디의 role 을 user 로 바꿔주면 된단다.

✅ 성공

  1. h2-console 로 가서 sql 적어준다.
SELECT * FROM USER;

UPDATE USER SET ROLE = 'USER';


SQL 은 "" 이거 하면 안되나보네 ''이거 하니까 됨.
"USER"
'USER'

USER 로 바꾼 뒤 한 번 로그아웃 해줘야한다. 로그아웃 안 하고 바로 하면 안된다.

로그아웃 하고 다시 로그인 해서 글을 써보면 등록된다.

매우 오래걸렸지만, 해냈다...!

정말 이해한건 10%도 안된다. 코드도 많은 대다가 다 생소한 코드다. 힘들다 힘들어. 이걸 다음 프로젝트에 어떻게 적용시켜요..ㅠㅠ

점점 내 뇌가 힘들어한다. 더 힘든 건 아직 책 반도 안 지났다.😔

profile
BEAT A SHOTGUN

0개의 댓글