org.springframework.validation.Errors
스프링에서 유효성 검사(validation) 결과를 저장하고 처리하기 위한 인터페이스
BindingResult가 상속하고 있는 상위 인터페이스이다.
검증 대상 객체에 발생한 에러 메시지, 필드 오류 정보, 글로벌 오류 정보를 담을 수 있다.
| 메서드 | 설명 |
|---|---|
reject(String errorCode) | 글로벌 오류 등록 |
rejectValue(String field, String errorCode) | 특정 필드에 대한 오류 등록 |
hasErrors() | 전체 오류 여부 확인 |
hasFieldErrors(String field) | 특정 필드에 오류가 있는지 확인 |
getAllErrors() | 모든 오류 가져오기 |
getFieldErrors() | 필드별 오류 가져오기 |
@Component
public class UserFormValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return UserForm.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
UserForm form = (UserForm) target;
if (!form.getPassword().equals(form.getConfirmPassword())) {
// 유효성 검사 실패를 직접 등록
errors.rejectValue("confirmPassword", "PasswordsNotMatch", "비밀번호가 일치하지 않습니다");
}
}
}
@Valid나@Validated로 유효성 검사를 수행할 때, 검사 결과(에러 여부, 에러 메시지 등)를 담는 객체
에외로 처리되지 않고, 컨트롤러에서 직접 오류를 처리할 수 있다.
만약 BindingResult 없이 유효성 검사에 실패하면 MethodArgumentNotValidException 발생
BindingResult 사용한다면 예외 대신 결과를 확인하고 직접 처리 할 수 있다.
@Valid나@Validated뒤에 바로BindingResult가 와야 제대로 작동
// 올바른 순서
public String submit(@Valid UserDto dto, BindingResult bindingResult)
// 잘못된 순서 - 예외 발생
public String submit(BindingResult bindingResult, @Valid UserDto dto)
Java Bean Validation 을 수행하는 어노테이션으로 DTO나 폼 객체에 붙여서 자동 유효성 검사를 수행한다.
@PostMapping("/save")
public String save(@Valid UserForm form, BindingResult bindingResult) { ... }
| 구분 | 설명 |
|---|---|
@Valid + Errors | ✔ 사용 가능. Spring MVC 초기 예제에서 종종 등장. |
@Valid + BindingResult | ✔ 권장 방식. 현재 Spring Boot/JPA 기반 예제에서 표준처럼 사용됨. |
| Spring Framework | Errors 또는 BindingResult 모두 사용 가능 |
| Spring Boot | 기본적으로 BindingResult를 사용함 |
객체와 객체 사이의 관계를 데이터베이스의 테이블 관계에 맞춰 매핑하는 것
JPA나 Hibernate 같은 ORM 프레임워크에서 핵심적인 개념이다.
이 둘을 연결시켜주는 게 연관 관계 매핑이다.
객체(자바)
| 연관 관계 | 방향 | 설명 | 예시 | 비고 |
|---|---|---|---|---|
@OneToOne | 단방향 or 양방향 | 한 객체가 하나의 객체만 참조. 서로 1:1 관계 | 사람 ↔ 주민등록증 Person ↔ IdCard | @JoinColumn 필수 지연로딩(LAZY) 기본값은 EAGER |
@OneToMany | 보통 양방향 | 하나의 객체가 여러 객체를 참조 | 팀 → 팀원들 Team → List<Member> | 연관관계의 주인이 아님 (mappedBy 사용)단방향은 비효율적 (중간 테이블 생성됨) |
@ManyToOne | 단방향 or 양방향 | 여러 객체가 하나의 객체를 참조 | 여러 팀원 → 한 팀 Member → Team | 가장 많이 사용됨 외래키 가짐 = 연관관계 주인 |
@ManyToMany | 거의 양방향 | 여러 객체가 여러 객체를 참조 | 학생 ↔ 수업 Student ↔ Subject | 중간 테이블 자동 생성됨 실무에선 거의 사용 안 함, 중간 엔티티로 풀어냄 |
// @OneToOne
// 단방향 or 양방향
@Entity
public class Person {
@OneToOne
@JoinColumn(name = "id_card_id")
private IdCard idCard;
}
// @OneToMany + mappedBy
// 보통 양방향
@Entity
public class Team {
@OneToMany(mappedBy = "team")
private List<Member> members;
}
// @ManyToOne
// 단방향 or 양방향
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "team_id") // 외래키
private Team team;
}
// @ManyToMany
// 거의 양방향
@Entity
public class Student {
@ManyToMany
@JoinTable(name = "student_subject",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "subject_id"))
private List<Subject> subjects;
}
연관 관계가 설정된 다른 엔티티를 언제 DB에서 불러올지 설정하는 것
LAZY : 지연 로딩 (Lazy Loading) - 실제로 접근할 때 가져옴 (필요할 때만 쿼리 발생)EAGER : 즉시 로딩 (Eager Loading) - 엔티티를 조회할 때 즉시 함께 로딩 (즉시 쿼리 발생)@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY) // Fetch 전략
private Team team;
}
Member member = em.find(Member.class, 1L); // 여기선 아직 Team 조회 안함
Team team = member.getTeam(); // 이 시점에 Team 쿼리가 실행됨 (LAZY일 경우)
| 관계 어노테이션 | 기본 Fetch 전략 | 설명 |
|---|---|---|
@ManyToOne | LAZY | 실무에서 가장 많이 쓰는 관계 (성능 고려) |
@OneToOne | EAGER | 성능 문제 때문에 직접 LAZY로 변경 권장 |
@OneToMany | LAZY | 리스트는 무거우니 기본이 LAZY |
@ManyToMany | LAZY | 역시 기본 LAZY |
@OneToOne, @ManyToOne은 기본값이 EAGER여서 성능 저하가 발생할 수 있다.
Member member = em.find(Member.class, 1L);
EAGER일 경우 Member와 Team을 무조건 JOIN해서 한 번에 가져온다.
하지만 나중에 Team이 필요 없을 수도 있는데도 쿼리가 실행된다. (불필요한 성능 낭비)
대부분 fetch = FetchType.LAZY로 설정하고, 필요한 경우에만 JOIN FETCH로 명시적 쿼리를 작성하는 방식이 안정적이다.
JPA의 JPQL(객체지향 쿼리 언어)에서 사용하는 문법으로, 지연 로딩(LAZY)된 연관된 엔티티를 즉시 함께 조회할 때 사용된다.
Member member = em.find(Member.class, 1L);
Team team = member.getTeam(); // 이 시점에 추가 쿼리 발생
getTeam()을 호출할 때 추가 쿼리가 실행됨 → N+1 문제 발생 가능
JPQL에서 JOIN FETCH를 사용하면 연관된 엔티티를 한 번에 함꼐 조회 가능
String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
Member를 조회하면서 Member.team도 한 번의 쿼리로 함께 조회
이때 지연 로딩 무시하고 즉시 로딩
-> SQL로 변환되면 INNER JOIN team처럼 조인된 쿼리가 나간다.
1개의 쿼리로 N개의 결과를 조죄했는데, 그 결과의 연관된 엔티티를 가져오기 위해 추가로 N개의 쿼리가 발생하는 현상
총 1 + N 번의 쿼리가 실행됨
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
}
List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class)
.getResultList();
for (Member member : members) {
System.out.println(member.getTeam().getName()); // Team 접근
}
첫 번째 쿼리 -> Member 리스트 조회 (1건)
SELECT * FROM member;
반복문 내부에서 팀 이름 조회 시, 멤버마다 추가 쿼리 (N건)
SELECT * FROM team WHERE id = ?; <- N번 반복
1 + N개의 쿼리 발생
List<Member> members = em. createQuery(
"SELECT m FROM Member m JOIN FETCH m.team", Member.class
).getResultList();
for (Member member : members) {
System.out.println(member.getTeam().getName()); // 추가 쿼리 없음
}
-> 실행되는 쿼리
SELECT m.*, t.* FROM member m JOIN team t ON m.team_id = t.id;
오늘은 @Valid와 연관 관계 매핑에 대해 공부했다.
데이터베이스에서 데이터를 조회하는 방식과 밀접한 개념이라 중요한 부분이라고 느꼈다.
JPQL을 공부하면서 SELECT member FROM Member member 구문을 보게 되었는데,
SQL에서는 보통 SELECT *을 사용하는 반면, JPQL에서는 SELECT member처럼 엔티티 자체를 조회한다.
이는 JPQL이 테이블이 아닌 자바의 엔티티 객체를 대상으로 질의하기 때문이며, 결과로 객체 자체가 반환된다는 의미다.