https://docs.spring.io/spring-data/jpa/reference/jpa.html
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);
}
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);
}
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);
}
}
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
인증은 누구인지 판단하는 것 ex. 카페 가입을 해야 글을 볼 수 있음
승인은 등급에 따라 볼 수 있는 범위가 다른 것 ex. 카페에서 특정 등급을 도달해야지만 볼 수 있는 거
| 구분 | Authentication (인증) | Authority(승인) |
|---|---|---|
| 질문 | "당신은 누구입니까?" (신원 확인) | "당신은 무엇을 할 수 있습니까?" (권한 부여) |
| 비유 | 회사 사원증 태그 | '서버실' 문을 열 수 있는 권한 |
| 구현 | 로그인, 생체 인식, OTP | 특정 URL 접근 제어, 버튼 활성화/비활성화 |
| HTTP상태 (실패시) | 401 Unauthorized | 403 Forbidden |
부서

서비스를 운영하는 내부 직원들끼리 보는 관리자 전용 페이지
관리자 페이지와 사용자 페이지를 합칠 경우,
관리자 페이지와 사용자 페이지를 아예 따로 만들거나, 프론트엔드 프로젝트를 분리하는 방식
장점:
- 보안: 관리자 서버는 아예 외부 접속을 차단(방화벽)할 수 있어 훨씬 안전함.
- 안정성: 사용자가 몰려 메인 서버가 터져도, 관리자 서버는 살아있어 대응이 가능.
- 배포: 관리자 기능만 수정했을 때, 전체 서비스를 재배포할 필요가 없음.
단점: 초기 세팅 비용이 들고, 도메인 객체(Entity) 등을 공유하기 위한 '공통 모듈(Library)' 관리가 필요할 수 있음.
그렇다면, 분리 기준이 어떻게 될까?
보안 요구 사항이 강조될때(VPN / 내부망), 배포 주기가 서로 다를때, 프론트 스택이 다를때, 성능 이슈(sql), 팀 규모 확장(백엔드 5명 이상, 프론트 2명 이상)
빌드 배포가 10분 넘어갈때
동시 접속자수 1,000명 이상일 경우
관리자 기능이 30% 이상일 경우
용도가 다르기에 entity(DB 처리)와 dto는 구분을 해야 함. 비즈니스 로직은 항상 바뀌기에

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;
}
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;
}
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;
}
package com.mycom.myapp.user.dto;
import lombok.Data;
@Data
public class UserResultDto {
private String result;
private UserDto userDto;
}
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);
}
package com.mycom.myapp.auth.service;
import com.mycom.myapp.user.dto.UserResultDto;
public interface LoginService {
UserResultDto login(String email, String password);
}
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;
}
}
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;
}
}