11/26 JPA

AI·2025년 11월 26일

JPA

https://docs.spring.io/spring-data/jpa/reference/jpa.html

Repository

package com.mycom.myapp.repository;

import java.util.List;

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

import com.mycom.myapp.entity.Student;

public interface StudentRepository extends JpaRepository<Student, Integer>{
	// 기본적인 crud는 메소드 선언 없이 상속받은 인터페이스를 통해 구현
	// 검색 용도(select with 조건) findBy~는 선언만 하면 알아서 구현
	List<Student> findByName(String name);
	List<Student> findByEmailAndPhone(String email, String phone);
	List<Student> findByEmailOrPhone(String email, String phone);
	
	List<Student> findByNameStartingWith(String name);
	List<Student> findByEmailEndingWith(String email);
	List<Student> findByPhoneContaining(String phone);
	
	List<Student> findByNameLike(String name);
	
	List<Student> findAllByOrderByNameDesc();
	
	List<Student> findByIdBetween(int from, int to);
}

Service

package com.mycom.myapp.service;

import java.util.List;

import com.mycom.myapp.entity.Student;

public interface StudentServiceFind {
	List<Student> findByName(String name);
	List<Student> findByEmailAndPhone(String email, String phone);
	List<Student> findByEmailOrPhone(String email, String phone);
	
	List<Student> findByNameStartingWith(String name);
	List<Student> findByEmailEndingWith(String email);
	List<Student> findByPhoneContaining(String phone);
	
	List<Student> findByNameLike(String name);
	
	List<Student> findAllByOrderByNameDesc();
	
	List<Student> findByIdBetween(int from, int to);
}

ServiceImpl

package com.mycom.myapp.service;

import java.util.List;

import org.springframework.stereotype.Service;

import com.mycom.myapp.entity.Student;
import com.mycom.myapp.repository.StudentRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class StudentServiceFindImpl implements StudentServiceFind {
	private final StudentRepository studentRepository;

	@Override
	public List<Student> findByName(String name) {
		return studentRepository.findByName(name);
	}

	@Override
	public List<Student> findByEmailAndPhone(String email, String phone) {
		return studentRepository.findByEmailAndPhone(email, phone);
	}

	@Override
	public List<Student> findByEmailOrPhone(String email, String phone) {
		return studentRepository.findByEmailOrPhone(email, phone);
	}

	@Override
	public List<Student> findByNameStartingWith(String name) {
		return studentRepository.findByNameStartingWith(name);
	}

	@Override
	public List<Student> findByEmailEndingWith(String email) {
		return studentRepository.findByEmailEndingWith(email);
	}

	@Override
	public List<Student> findByPhoneContaining(String phone) {
		return studentRepository.findByPhoneContaining(phone);
	}

	@Override
	public List<Student> findByNameLike(String name) {
//		return studentRepository.findByNameLike(name); // equal과 동일
//		return studentRepository.findByNameLike(name+"%"); // starttinf with
//		return studentRepository.findByNameLike("%"+name); // ending with
		return studentRepository.findByNameLike("%"+name+"%"); // containing
	}

	@Override
	public List<Student> findAllByOrderByNameDesc() {
		return studentRepository.findAllByOrderByNameDesc();
	}

	@Override
	public List<Student> findByIdBetween(int from, int to) {
		return studentRepository.findByIdBetween(from, to);
	}

}

controller

package com.mycom.myapp.controller;

import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.mycom.myapp.entity.Student;
import com.mycom.myapp.service.StudentServiceFind;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/students")
@RequiredArgsConstructor
public class StudentControllerFind {
	private final StudentServiceFind studentServiceFind;
	
	@GetMapping("/find/name")
	public List<Student> findByName(@RequestParam("name") String name){
		return studentServiceFind.findByName(name);
	}
	
	@GetMapping("/find/emailandphone")
	public List<Student> findByEmailAndPhone(
			@RequestParam("email") String email, 
			@RequestParam("phone") String phone){
		return studentServiceFind.findByEmailAndPhone(email, phone);
	}
	
	@GetMapping("/find/emailorphone")
	public List<Student> findByEmailOrPhone(
			@RequestParam("email") String email, 
			@RequestParam("phone") String phone){
		return studentServiceFind.findByEmailOrPhone(email, phone);
	}
	
	@GetMapping("/find/namestartingwith")
	public List<Student> findByNameStartingWith(@RequestParam("name") String name){
		return studentServiceFind.findByNameStartingWith(name);
	}
	
	@GetMapping("/find/emailendingwith")
	public List<Student> findByEmailEndingWith(@RequestParam("email") String email){
		return studentServiceFind.findByEmailEndingWith(email);
	}
	
	@GetMapping("/find/phonecontaining")
	public List<Student> findByPhoneContaining(@RequestParam("phone") String phone){
		return studentServiceFind.findByPhoneContaining(phone);
	}
	
	@GetMapping("/find/namelike")
	public List<Student> findByNameLike(@RequestParam("name") String name){
		return studentServiceFind.findByNameLike(name);
	}
	
	@GetMapping("/find/orderbyname")
	public List<Student> findAllByOrderByNameDesc(){
		return studentServiceFind.findAllByOrderByNameDesc();
	}
	
	@GetMapping("/find/idbetween")
	public List<Student> findByIdBetween(
			@RequestParam("from") int from, 
			@RequestParam("to") int to) {
		return studentServiceFind.findByIdBetween(from, to);
	}

}

권한 관리

일반 사용자, 관리자 -> role based Authority

Authentication(인증) vs Authority(승인)

인증은 누구인지 판단하는 것 ex. 카페 가입을 해야 글을 볼 수 있음
승인은 등급에 따라 볼 수 있는 범위가 다른 것 ex. 카페에서 특정 등급을 도달해야지만 볼 수 있는 거

구분Authentication (인증)Authority(승인)
질문"당신은 누구입니까?" (신원 확인)"당신은 무엇을 할 수 있습니까?" (권한 부여)
비유회사 사원증 태그'서버실' 문을 열 수 있는 권한
구현로그인, 생체 인식, OTP특정 URL 접근 제어, 버튼 활성화/비활성화
HTTP상태
(실패시)
401 Unauthorized403 Forbidden

Role Based Authority

부서

Back Office

서비스를 운영하는 내부 직원들끼리 보는 관리자 전용 페이지

관리자용, 일반사용자용 application 구조

- 하나로 vs 분리

관리자 페이지와 사용자 페이지를 합칠 경우,

  • 장점: 개발 속도가 빠르다. User 엔티티나 Service 로직을 공유해서 쓸 수 있어 코드 중복이 없다. 초기 스타트업이나 소규모 프로젝트에 적합하다.
  • 단점: 관리자 기능 코드가 일반 사용자용 코드와 섞여 있어, 실수로 관리자 API가 외부에 노출될 위험이 있다. 프로젝트 덩치가 커집니다.

관리자 페이지와 사용자 페이지를 아예 따로 만들거나, 프론트엔드 프로젝트를 분리하는 방식

  • 장점:
    - 보안: 관리자 서버는 아예 외부 접속을 차단(방화벽)할 수 있어 훨씬 안전함.
    - 안정성: 사용자가 몰려 메인 서버가 터져도, 관리자 서버는 살아있어 대응이 가능.
    - 배포: 관리자 기능만 수정했을 때, 전체 서비스를 재배포할 필요가 없음.

  • 단점: 초기 세팅 비용이 들고, 도메인 객체(Entity) 등을 공유하기 위한 '공통 모듈(Library)' 관리가 필요할 수 있음.

그렇다면, 분리 기준이 어떻게 될까?
보안 요구 사항이 강조될때(VPN / 내부망), 배포 주기가 서로 다를때, 프론트 스택이 다를때, 성능 이슈(sql), 팀 규모 확장(백엔드 5명 이상, 프론트 2명 이상)
빌드 배포가 10분 넘어갈때
동시 접속자수 1,000명 이상일 경우
관리자 기능이 30% 이상일 경우

실습

용도가 다르기에 entity(DB 처리)와 dto는 구분을 해야 함. 비즈니스 로직은 항상 바뀌기에

user

package com.mycom.myapp.user.entity;

import java.util.List;

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

// 로그인 시점(Authorization이 완료)에 Authentication 처리를 위해
// 로그인한 사용자의 role을 가져옴(즉시 eager fetch)
// user 중심으로 userrole 사용
// user 엔티티가 중심 ownership 가짐 @OneToMany
@Entity
@Getter
@Setter
@ToString
public class User {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	private Long id;
	private String name;
	private String email;
	private String password;
	@OneToMany(fetch=FetchType.EAGER)
//	@OneToMany
	@ToString.Exclude // toString에서 빼고 출력하라는 의미
	private List<UserRole> userRoles;
}

userRole

package com.mycom.myapp.user.entity;

import java.util.List;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

// userrole을 어떤 user가 가지고 있고, 이와 관련된 back office 업무를 수행한다면
// @ManyToMany로 가는 것이 바람직하나, 현재는 Authorization 용도로 사용
@Entity
@Getter
@Setter
@ToString
public class UserRole {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	private Integer id;
	private String name;
}

userDto

package com.mycom.myapp.user.dto;

import java.util.List;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class UserDto {
	private Long id;
	private String name;
	private String email;
	private String password;
	// client에게 전달되는 userdto에 userrole entity 전체 필요x
	// entity와 dto기 별도로 구현되어서 각각의 BL에 의해 변화될 수 있도록 처리
	private List<String> roles;
}

UserResultDto

package com.mycom.myapp.user.dto;

import lombok.Data;

@Data
public class UserResultDto {
	private String result;
	private UserDto userDto;
}

UserRepository

package com.mycom.myapp.user.repository;

import java.util.Optional;

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

import com.mycom.myapp.user.entity.User;

public interface UserRepository extends JpaRepository<User, Long>{
	// 기본 crud
	
	// 로그인 email, password 검증, repository단계에서는 email로 select -> pw검사
	// => findByEmail
	// user package 내 다른 업무를 추가하면 메소드가 늘어나게 되고 
	// 그 중 하나인 findByEmail은 로그인 도메인 관련보다는 회원 도메인 메소드 중 하나가 됨
	// auth package에서 메소드를 사용해서 user entity 정보를 얻음
	
	// loginServiceImpl의 #1
//	Optional<User> findByEmail(String email);
	
//	@Query("select u from User u join fetch u.userRoles where u.email=:email") // fetch eager
//	@Query("select u from User u join u.userRoles where u.email=:email")
	@Query("select u from User u where u.email=:email")
	Optional<User> findByEmail(@Param("email") String email);
}

LoginService

package com.mycom.myapp.auth.service;

import com.mycom.myapp.user.dto.UserResultDto;

public interface LoginService {
	UserResultDto login(String email, String password);
}

LoginServiceImpl

package com.mycom.myapp.auth.service;

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

import org.springframework.stereotype.Service;

import com.mycom.myapp.user.dto.UserDto;
import com.mycom.myapp.user.dto.UserResultDto;
import com.mycom.myapp.user.entity.User;
import com.mycom.myapp.user.repository.UserRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService {

	private final UserRepository userRepository;
	@Override
	public UserResultDto login(String email, String password) {
		UserResultDto userResultDto = new UserResultDto();
		
		// #1. findbyemail 
		// select 2번 수행
		// user, userRole 관계 테이블이 id로 연결되어 있고
		// findbyemail을 통해서 user fetch할 때, user id 확보한 후에 role을 select 해야 함
//		Optional<User> optionalUser = userRepository.findByEmail(email);
		
		// user id로 찾으면 이미 role에 필요한 id를 가졌기에 바로 join(fetch eager)
		// #2. hardcoding value
//		Optional<User> optionalUser = userRepository.findById(3L);
		
		Optional<User> optionalUser = userRepository.findByEmail(email);
		
		if(optionalUser.isPresent()) {
			User user = optionalUser.get();
			if(user.getPassword().equals(password)) {
				// userRole 객체들 -> UserRole의 이름(string)만 추출
				List<String> roles = new ArrayList<>();
				user.getUserRoles().forEach(userRole -> roles.add(userRole.getName()));
				// userEntity 객체 -> userDto 객체
				// password는 생략(front까지 전달)
				UserDto userDto = UserDto.builder()
						.id(user.getId())
						.name(user.getName())
						.email(user.getEmail())
						.roles(roles)
						.build();
				userResultDto.setUserDto(userDto);
				userResultDto.setResult("success");
			}else {
				userResultDto.setResult("wrong pw");
			}
		} else {
			userResultDto.setResult("fail");
		}
		return userResultDto;
	}

}

LoginController

package com.mycom.myapp.auth.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.mycom.myapp.auth.service.LoginService;
import com.mycom.myapp.user.dto.UserResultDto;

import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class LoginController {
	private final LoginService loginService;
	
	@PostMapping("/login")
	public UserResultDto login(
			@RequestParam("email") String email,
			@RequestParam("password") String password,
			HttpSession session) {
		UserResultDto userResultDto = loginService.login(email, password);
		if(userResultDto.getResult().equals("success")) {
			session.setAttribute("userDto", userResultDto.getUserDto());
		}
		
		return userResultDto;
	}
}

0개의 댓글