[SSAFYcial: Spring] JPA로 구현한 싸피 백엔드 개발
A A

안녕하세요!

12기 싸피셜 이서현입니다. 😊

 

2학기 첫 프로젝트였던 공통프로젝트가 끝난 지 한 달이 지났는데요,

엊그제 개강한 것 같은데 벌써 특화프로젝트 진행중이라니...

 

거짓말

 

 

시간이 정말 너무 빠르네요^^

이걸 한 번만 더 반복하면 싸피도 끝이라니 믿고 싶지 않은 현실입니다. (싸피야 사랑해)

 

 

 

첫 프로젝트에서 저는 백엔드를 담당하게 되었는데요, 

1학기 때 MyBatis를 썼던 것과는 달리, 이번에는 Spring JPA를 사용하게 되어서

블로그에도 소개하고 싶어 이렇게 작성하고 있답니다~!

 

 


 

JPA(Java Persistence API)란?

자바의 ORM(Object-Relational Mapping) 기술을 쉽게 구현하도록 도와주는 API입니다.

JpaRepository를 상속하는 인터페이스에 메서드 이름만 작성하면,

JPA가 구현체를 생성해 주고 필요한 쿼리문을 자동으로 처리해 줍니다.

 

DB 관련해서는 Entity라는 클래스를 작성해서 매핑합니다.

(엔티티 클래스 작성하고 실행시키면 DB 안에 테이블이 자동 생성된답니다... 신기하죠?)

 

따라서 기본적인 패키지로는

Domain (Entity): DB 매핑
Repository: DB에 접근해서 데이터를 저장, 조회, 수정, 삭제
Service: 비즈니스 로직 담당. 여러 Repository 조합해서 처리
Controller: 클라이언트 요청 받아서 Service 호출, 응답을 반환 

 

등이 구성됩니다.

 



Spring Data JPA Repository 방식은?

Spring Data JPA가 제공하는 자동 쿼리 생성 기능과 CRUD 메서드를 활용하는 방식입니다.

이게 엄청엄청 편해서 저는 너무 잘 활용했어요! 

 

public interface StoreEmployeeRepository extends JpaRepository<StoreEmployee, Long> {
    boolean existsByStoreAndUser(Store store, User user);
}

 

보기만 해도 정말 짧지 않나요?

 

 

 

 

Spring Data JPA의 특징

1. 인터페이스 기반 정의

직접 구현체를 작성하지 않아도, JpaRepository를 상속받아서 인터페이스만 정의하면 됩니다.

 

 

2. 자동 쿼리 메서드 생성

메서드 이름만으로도 원하는 쿼리를 작성할 수 있어요!

ex. existByStoreAndUserStore과 User를 조건으로 존재 여부를 확인하는 쿼리입니다. 직관적이죠? 

 

 

3. @Repository 생략 가능

SpringBoot에서는 Repository 인터페이스에 @Repository를 생략해도 Spring이 자동으로 관리합니다.

 

 

 

 


 

 

 

 

뭐? 메서드 이름만 지어도 쿼리가 자동 생성된다고?

 

쿼리문을 직접 작성하지 않아도 된다니 감동이죠...

(MyBatis 환경에서 오타 삐끗했다가 눈 빠질 뻔한 적 있는 사람... 손 들어)

 

이 좋은 걸 나만 몰랐다고 아햄들다

 

 

 

그러면 메서드 이름을 정하기 위한 특정한 형식이 있나?

 

 

저는 이 부분이 궁금해졌습니다! 

 

 

 

 

 

 

 

 

기본 메서드 이름 형식

findBy + [필드명] + [조건] + [연결 조건]

 

위와 같은 형식으로 전개되는데요,

자세한 요소는 다음과 같습니다.

 

 

 

 

주요 키워드와 예제

 

1. 기본 키워드

findBy: 데이터를 조회

existsBy: 데이터 존재 여부 확인

countBy: 데이터 개수 조회

deleteBy: 데이터 삭제

 

 

2. 필드명

Entity 클래스에서 정의된 필드명을 사용해야 합니다.

가령 User Entity에 Name, Age 등의 필드가 있다면 다음과 같이 쓸 수 있겠죠?

예: findByName, findByAge

 

 

3. 조건 키워드

 

 

4. 연결 조건

필드나 조건이 여러 개라면, And 혹은 Or로 조합해서 사용합니다.

ex: findByNameAndAge(String name, int age)...는 name과 age의 조건을 모두 만족했을 때,

findByNameOrAge(String name, int age)...는 둘 중 하나라도 만족할 때! 라고 할 수 있습니다.

 

 

 

 




 

그러면, 제가 실제로 작성했던 코드를 예시로 보여드리겠습니다.

 

 

1. User (domain)

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Getter @Setter
public class User {

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

    private String email;

    private String password;

    private String userName;

    private String phone;

    @Enumerated(EnumType.STRING)
    private UserRole role; // ENUM[OWNER, STAFF]

    private LocalDateTime createdAt;

    // ... 생략 
}

 

위와 같이 작성해서 User Entity, DB상 테이블을 생성해 줍니다.

 

 

2. UserRepository (repository)

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmail(String email);
    Optional<User> findByUserId(Long userId);
    Optional<User> deleteByUserId(Long userId);

    //...생략 

}

 

findByEmail -> email을 기준으로 User를 찾는 메서드,

findByUserId -> userId를 기준으로 User를 찾는 메서드,

deleteByUserId -> userId를 기준으로 User를 지우는 메서드

 

직관적으로 이해할 수 있고, 구현도 간편해집니다.

 

 

3. UserService (service)

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.d109.reper.domain.User;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);

    private final PasswordEncoder passwordEncoder;



    // 회원가입
    public Long insertMember(User user) {
        // 패스워드 암호화
        user.setPassword(passwordEncoder.encode(user.getPassword()));

        // User를 저장하고, 저장된 User가 null이 아니면 성공으로 판단
        User savedUser = userRepository.save(user);
        logger.info("저장된 User: {}", savedUser);
        return savedUser != null ? savedUser.getUserId() : null;
    }


    // 아이디 중복 확인
    public boolean isEmailDuplicate(String email) {
        if (email == null || email.isBlank()) {
            throw new IllegalArgumentException("InvalidRequest: 파라미터가 전달되지 않음.");
        }

        if (!email.matches("^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
            throw new IllegalArgumentException("InvalidFormat: 파라미터의 형식이 유효하지 않음.");
        }

        return userRepository.findByEmail(email).isPresent();
    }


    //로그인 유효성 검증
    public User validateLogin(String email, String password) {
        User user = userRepository.findByEmail(email)
                .orElseThrow(() -> new SecurityException("InvalidCredentials"));

        if (!passwordEncoder.matches(password, user.getPassword())) {
            logger.warn("비밀번호 불일치 - email: {}", email);
            throw new SecurityException("InvalidCredentials");
        }

        return user;
    }


    // 이메일 기준으로 사용자 정보 찾기
    public User findByEmail(String email) {
        try {
            User user = userRepository.findByEmail(email)
                    .orElseThrow(() -> new NoSuchElementException("UserNotFound"));
            return user;
        } catch (Exception e) {
            throw new IllegalArgumentException("Bad Request");
        }
    }

    // 회원 탈퇴
    @Transactional
    public boolean deleteUser(Long userId) {
        Optional<User> user = userRepository.findByUserId(userId);

        if (user.isPresent()) {
            userRepository.deleteByUserId(userId);
            return true;
        } else {
            return false;
        }
    }
    
    
    //...생략 
}

 

회원가입을 위한 아이디 중복 확인, 회원 탈퇴 등에

repository의 메서드가 활용되었습니다.

 

 

4. UserController

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Slf4j
public class UserController {

    private final UserService userService;
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);


    //회원가입
    @PostMapping
    @Operation(summary = "사용자 정보를 추가합니다. 성공하면 저장된 userId를 리턴합니다. ", description = "모든 정보를 입력해야 회원가입이 가능합니다.")
    public ResponseEntity<?> join(@RequestBody JoinRequest joinRequest) {
        log.info("insertMember::: {}", joinRequest);

        // 이메일 중복 확인
        if (userService.isEmailDuplicate(joinRequest.getEmail())) {
            throw new IllegalArgumentException("이메일이 이미 존재함.");
        }

        User user = new User();
        user.setEmail(joinRequest.getEmail());
        user.setPassword(joinRequest.getPassword());
        user.setUserName(joinRequest.getUserName());
        user.setPhone(joinRequest.getPhone());
        user.setRole(joinRequest.getRoleEnum());  // Enum 변환

        Long userId = userService.insertMember(user);

        if (userId != null) {
            return ResponseEntity.ok(userId);
        } else {
            throw new RuntimeException("회원가입 실패");
        }
    }


    // 이메일 중복 확인
    @GetMapping("/email/check-duplication")
    @Operation(summary = "중복 email이면 true를 반환합니다.")
    public ResponseEntity<Boolean> checkEmailDuplication(@RequestParam(value = "email") String email) {
        if (email == null || email.isEmpty()) {
            throw new IllegalArgumentException("파라미터가 전달되지 않았습니다.");
        }

        if (!email.matches("^[\\w.%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$")) {
            throw new IllegalArgumentException("파라미터의 형식이 유효하지 않습니다.");
        }

        boolean isDuplicate = userService.isEmailDuplicate(email);
        return ResponseEntity.ok(isDuplicate);
    }

//...생략

service에 이어 controller까지 작성해 주고 나면 

서비스를 위한 REST API가 완성됩니다.

 

 

참 쉽죠...?

 

 

이렇게나 편리한 JPA 많은 분들이 써 보시길 바라며...

다음 포스팅에서도 재미난 기술 소개를 들고 오도록 하겠습니다 ㅎㅎ

 

 

 

 

SSAFY 홈페이지 바로가기

SSAFYcial 인스타그램

 

 

 

 

 

Copyright 2024. GRAVITY all rights reserved