[졸프] 로그인 API 구현

순두누나·2025년 5월 8일

졸업프로젝트

목록 보기
2/21

그 전 회원가입 API에 이어서 로그인 API 구현 시작!
우선 토큰 생성기를 안 만들어놨기 때문에 JWT 토큰 생성기부터 구현 시작해봅니다.

1단계: JWT 토큰 생성

1. 의존성 추가 (build.gradle)

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

2. JwtTokenProvider 클래스 생성

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
public class JwtTokenProvider {

    private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // 256비트 키 생성
    private final long accessTokenValidTime = 1000L * 60 * 30; // 30분
    private final long refreshTokenValidTime = 1000L * 60 * 60 * 24 * 7; // 7일

    // Access Token 생성
    public String createAccessToken(Long userId) {
        return Jwts.builder()
                .setSubject(String.valueOf(userId))
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + accessTokenValidTime))
                .signWith(key)
                .compact();
    }

    // Refresh Token 생성
    public String createRefreshToken(Long userId) {
        return Jwts.builder()
                .setSubject(String.valueOf(userId))
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + refreshTokenValidTime))
                .signWith(key)
                .compact();
    }

    // 토큰에서 userId 추출
    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
        return Long.parseLong(claims.getSubject());
    }

    // 토큰 유효성 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

3. application.yml (비밀 키를 외부화)

jwt:
  secret: my-very-secret-jwt-key-that-should-be-long
  expiration: 3600000 //키 외부화와 만료 시간 설정
  • 이미 나는 이렇게 외부화와 만료시간을 다 완성시켜놨기 때문에 그대로 납둠!
  • 외부화는 하드코딩을 하는 것보다 보안이 훨씬 강화되므로 무조건 외부화 코딩을 하기!

4. AuthController 연결

앞서 만든 JwtTokenProvider를 @Autowired로 주입받아 사용

@RestController
@RequiredArgsConstructor
public class AuthController {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtTokenProvider jwtTokenProvider; //이 부분 추가

     // 로그인 API
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        Optional<User> optionalUser = userRepository.findByUserEmail(request.getUserEmail());
        if (optionalUser.isEmpty()) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("이메일 또는 비밀번호가 일치하지 않습니다.");
        }

        User user = optionalUser.get();

        if (!passwordEncoder.matches(request.getUserPassword(), user.getUserPassword())) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("이메일 또는 비밀번호가 일치하지 않습니다.");
        }

        String accessToken = jwtTokenProvider.createAccessToken(user.getUserId());

        return ResponseEntity.ok(new LoginResponse(accessToken));
    }

Getter 메소드 추가

위 과정을 실행한 후

이렇게 메소드가 존재하지 않는다는 오류 발생!
LoginRequest 클래스와 User 엔티티에 필요한 Getter 메소드가 없기 때문임.

즉,

  • LoginRequest.getUserEmail()
  • LoginRequest.getUserPassword()
  • User.getUserPassword()
  • User.getUserId()
    이 메소드들을 선언해주어야한다!

(1) LoginRequest 클래스 수정

// Getter
    public String getUserEmail() {
        return userEmail;
    }

    public String getUserPassword() {
        return userPassword;
    }

해당 코드 추가

(2)User 엔티티 클래스 수정

public Long getUserId() {
        return userId;
    }

    public String getUserPassword() {
        return userPassword;
    }

해당 코드 추가

UserId 형식 변환

그리고 여기서 하던 중에 userId는 int 형식이 아니라 long 형식으로 하는 것이 좋겠다고 판단!
1. 우선 ERD 수정
2. java 파일 수정
3. DB 파일 수정 (여기서 long을 BIGINT라고 적는 것을 처음 알았다)

500 에러 트러블슈팅

그러고 나서 우선 회원가입 API를 포스트맨에서 테스트 했을 때. 500 에러 발생
intellij 콘솔에서 확인한 결과

Unknown column 'u1_0.user_id' in 'field list'

라는 오류 발생.

이는 User 엔티티에서는 userId라고 정의했는데, 실제 DB 테이블의 컬럼 이름은 user_id로 되어 있지 않다는 뜻.

하지만..DB를 다시 확인해도

정상적으로 userId로 작성된 것 확인.

그래에서 엔티티에 @Column(name = "userId") 이 코드를 추가했지만 또 안됨.

다시 콘솔을 확인하니

Hibernate: 
    select
        u1_0.user_id,
        u1_0.birth_date,
        u1_0.gender,
        u1_0.name,
        u1_0.phone_number,
        u1_0.profile_url,
        u1_0.reading_taste,
        u1_0.user_email,
        u1_0.user_nickname,
        u1_0.user_password 
    from
        user u1_0 
    where
        u1_0.user_email=?

이런 코드가 나온다.

이거는 JPA가 네이밍 전략을 자동으로 적용하고 있기 때문!
Spring Boot는 기본적으로

org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

이 전략을 사용하는데 이는 하이픈 방식으로 자동 변환시킨다.

해결방법 : Hibernate 네이밍 전략 끄기

application.yml 파일에

spring:
  jpa:
    hibernate:
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

이 코드 수정

여기서 질문! 그러면 JPA는 스네이크 케이스 방식을 기본으로 가져가는 걸까?

Java: 카멜케이스 → userId
데이터베이스: 보통 스네이크케이스 → user_id

Hibernate는 이 차이를 맞춰주기 위해 다음과 같은 자동 네이밍 전략을 사용

userId → user_id
userEmail → user_email
방법설명권장 여부
DB 컬럼도 스네이크케이스 (user_id)로 만들기JPA 기본 전략과 일치함👍 일반적으로 권장
Java에 @Column(name=...)로 명시하기DB 컬럼명을 따로 쓰고 싶을 때 사용👍 추천
하이픈(user-id)❌ SQL/DB에서 지원 안 됨🚫 절대 비추천

🎯 왜 Java와 DB 스타일이 다를까?

Java는 camelCase가 표준 스타일이고,
SQL은 snake_case가 가독성 측면에서 일반적.

그래서 JPA는 이 둘의 차이를 자동으로 변환해주는 네이밍 전략을 사용한다.

🧩 “하나로 통일”하기 위한2가지 방법

1. JPA가 자동 변환하도록 맡기기 (관례 따르기)
DB는 snake_case (user_id)
Java는 camelCase (userId)

JPA가 자동으로 변환해줌 → 가장 관리 쉬움

2. @Column 애노테이션으로 명시적으로 하나하나 맞추기 (통일감 있음, 하지만 번거로움)
예: Java와 DB 모두 카멜케이스로 통일하고 싶다면:

  • DB에 userId라는 컬럼을 만들고
  • JPA에 다음처럼 명시
@Column(name = "userId")
private Long userId;
  • 이러면 Hibernate는 user_id 대신 userId를 찾음

실무에서는 JPA의 자동 네이밍 전략을 그대로 사용하는 방식 선호!

이상 트러블 슈팅은 마무리!
그걸 고치고 나니

이렇게 로그인 API 구현은 성공적이었다!!

profile
순두의 누나입니다

0개의 댓글