자바의 정석에서 생성자를 이용한 인스턴스 복사에 관해 공부를 하다
굳이 생성자를 이용해서 인스턴스 복사를 하는 이유가 뭐지에 대해서 생각해보게 됐다.
그러던 중 spring에서 생성자를 이용해서 인스턴스를 복사하고, 불변 패턴을 설계하는 방법과 연관이 있다는 걸 알게되어 포스팅하게 됐다.
1. 생성자를 이용한 인스턴스 복사(Copy Constructor)
개념
- 기존 객체를 기반으로 새로운 객체를 만들 때 사용
- 원본과 독립적인 객체를 생성 가능 → 깊은 복사(Deep Copy) 가능
- 얕은 복사(Shallow Copy)와 깊은 복사 비교가 핵심
예제 코드
class Address {
private String city;
public Address(String city) { this.city = city; }
public Address(Address other) { this.city = other.city; }
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
@Override
public String toString() { return city; }
}
class User {
private String name;
private Address address;
public User(String name, Address address) { this.name = name; this.address = address; }
public static User shallowCopy(User other) { return new User(other.name, other.address); }
public static User deepCopy(User other) { return new User(other.name, new Address(other.address)); }
public Address getAddress() { return address; }
@Override
public String toString() { return name + " (" + address + ")"; }
}
public class CopyExample {
public static void main(String[] args) {
Address address = new Address("Seoul");
User original = new User("Alice", address);
User shallow = User.shallowCopy(original);
User deep = User.deepCopy(original);
original.getAddress().setCity("Busan");
System.out.println("원본: " + original);
System.out.println("얕은 복사: " + shallow);
System.out.println("깊은 복사: " + deep);
}
}
실행 결과
원본: Alice (Busan)
얕은 복사: Alice (Busan)
깊은 복사: Alice (Seoul)
- 얕은 복사는 참조를 공유 → 원본 변경 시 복사본도 변경
- 깊은 복사는 새로운 객체 생성 → 독립적
2. clone() vs 생성자 복사
- clone(): Cloneable 구현 필요, 얕은 복사 기본, 깊은 복사 수동 필요
- 복사 생성자: 명시적, 안전, 가독성 좋음
- 실무에서는 생성자 기반 복사를 선호
예제
// clone() 기반 깊은 복사 vs 생성자 기반 복사
User deepClone = original.deepClone(); // clone() 사용
User constructorCopy = new User(original); // 생성자 사용
3. Builder + 복사 생성자 패턴 (불변 객체)
목적
- 불변 객체(Immutable Object) 설계
- 일부 속성만 변경해 새로운 객체 생성
- 실무에서는 Lombok @Builder(toBuilder = true)와 함께 사용
예제 코드
public class User {
private final String name;
private final int age;
private final Address address;
private User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.address = builder.address;
}
public Builder toBuilder() { return new Builder(this); }
public static class Builder {
private String name;
private int age;
private Address address;
public Builder() {}
public Builder(User other) {
this.name = other.name;
this.age = other.age;
this.address = new Address(other.address);
}
public Builder name(String name) { this.name = name; return this; }
public Builder age(int age) { this.age = age; return this; }
public Builder address(Address address) { this.address = address; return this; }
public User build() { return new User(this); }
}
}
- 기존 객체 기반으로 일부만 수정 가능
User modified = original.toBuilder()
.age(30)
.build();
4. Lombok + JPA Entity 환경
Entity 설계
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(toBuilder = true)
public class User {
@Id @GeneratedValue
private Long id;
private String name;
private int age;
private String city;
public User updateAge(int newAge) { return this.toBuilder().age(newAge).build(); }
public User updateCity(String newCity) { return this.toBuilder().city(newCity).build(); }
public UserDto toDto() {
return UserDto.builder()
.id(id).name(name).age(age).city(city).build();
}
}
- setter 없이 불변 패턴 유지
- toBuilder() + 수정 메서드로 안전하게 값 변경
Service + Controller 흐름
User user = userRepository.findById(id).orElseThrow();
User updatedUser = user.updateAge(30);
userRepository.save(updatedUser);
return updatedUser.toDto();
5. DTO 분리 이유
- Entity = DB 중심
- DTO = API/외부 요구사항 중심
- 장점: 보안, 불변성, 계층 분리, 유연성
@Getter @Builder
public class UserRequestDto { private String name; private int age; private String city; }
@Getter @Builder
public class UserResponseDto { private Long id; private String name; private int age; private String city; }
- Entity → DTO 변환 메서드 활용
public UserResponseDto toResponseDto() {
return UserResponseDto.builder()
.id(id).name(name).age(age).city(city).build();
}
6. 전체 실무 패턴 요약
- 불변 Entity 설계 (setter 없음, @Builder(toBuilder = true))
- 값 변경 시 → 기존 객체 기반 toBuilder() + 수정 메서드 → 새 객체 생성
- DB 반영 → save() 호출
- Controller/Service 통신 → Entity → DTO 변환
- DTO 사용 → API 보안, 계층 분리, 테스트 용이
Client → Controller → Service → Repository → DB
↑ ↑
DTO Entity
←-------------------

'Java' 카테고리의 다른 글
| [Java] this와 super 이해하기 – 멤버 변수와 지역 변수의 차이 (0) | 2025.10.17 |
|---|---|
| [Java] 상속의 정의 (0) | 2025.10.17 |
| [Java] this()(생성자에서의 this 호출), this(참조변수로 자기 자신을 가리키는 경우) (0) | 2025.10.17 |
| [JAVA] 객체지향개념Ⅱ (0) | 2025.10.13 |
| [Java] 문자열 처리 성능비교 - String vs StringBuilder vs StringBuffer (0) | 2025.10.11 |