최근에 SSAFY를 통해 한 기업에서 면접을 보게 되었다.
풀스택을 뽑는 자리였고 코딩테스트를 통과해서 1차 면접을 진행했는데, 백엔드 위주로 구성된 포폴과 대답으로 기술 면접은 백엔드 위주로 들어왔다.
근데 질문에 대답을 못했던 부분이 많았어서 이번에 받은 질문들을 정리해보려고 한다.
1. 불변 객체가 무엇인가요?
내가 아는 불변 객체는 final을 이용해서 값을 변경하지 못하도록 만드는 객체를 의미한다고 생각했다.
그런데 이런 대답을 원하는 느낌이 아니었다...
불변 객체(Immutable Object)
한번 객체를 생성하면 그 상태를 바꿀 수 없는 객체를 의미한다. 반대로는 가변(mutable) 객체가 존재한다.
불변 객체는 read-only 메서드만 제공하며, 객체의 내부 상태를 제공하지 않거나 방어적 복사를 통해 제공한다.
여기서 방어적 복사 vs 깊은 복사 vs 얕은 복사의 개념이 나오게 된다.
1. 얕은 복사
heap 영역에 있는 객체를 복사할 때, 새로운 객체를 만들지만 원본 객체의 '주소 값'을 참조한다.
따라서 원본이나 복사한 객체가 변경이 되면 서로 영향을 미치게 된다.
public class Main {
public static class Car {
private String name;
private int price;
public Car(String name, int price) {
this.name = name;
this.price = price;
}
}
public static void main(String[] args) throws IOException {
Car car1 = new Car("소나타", 15000);
Car car2 = car1;
}
}
위에서 Car라는 클래스 안에 이름과 가격을 저장해놓고 main에서 car1을 생성하고 car2에 car1을 대입했다.
이렇게 대입하면 얕은 복사가 일어나게 된다. 밑에 그림으로 같이 어떻게 되는지 확인해보자.
위는 얕은 복사가 일어날 때 stack 영역과 heap 영역을 그림으로 그린 것이다.
car1를 만드는 시점에 heap 영역에 객체가 생성되고 stack 영역에 car1 변수가 생성된다.
이후 car2에 car1를 얕은 복사를 진행하면 동일한 주소값을 참조하기 때문에 같은 데이터를 바라보게 된다.
자 그러면 아래 코드의 결과는 뭐가 나올까?
public static void main(String[] args) throws IOException {
Car car1 = new Car("소나타", 15000);
Car car2 = car1;
car2.setName("제네시스");
System.out.println(car1.getName());
}
당연하게도 얕은 복사니깐 원본의 값도 변경된 것을 알 수 있다.
2. 깊은 복사
원본 객체를 복사할 때, 새로운 객체를 만드는데 원본의 모든 값을 복사해서 독립적인 객체로 만든다.
즉 서로 영향을 미치지 않는다.
깊은 복사를 구현하기 위해서는 Cloneable 인터페이스를 구현해야 한다.
내용을 보면 clone() 라는 메서드를 구현해야 한다고 나와있다. 또한 예외처리도 필수.
@Override
public Car clone() throws CloneNotSupportedException {
return (Car) super.clone();
}
Car 클래스에 Cloneable을 구현하고 clone() 메서드를 오버라이드 해주었다.
public static void main(String[] args) throws IOException, CloneNotSupportedException {
Car car1 = new Car("소나타", 15000);
Car car2 = car1.clone();
car2.setName("제네시스");
System.out.println(car1.getName());
System.out.println(car2.getName());
}
그리고 clone() 메서드를 통해 car2를 만들어주고 car2의 값을 변경하면 어떤 일이 일어날까?
car1과 car2는 독립적인 객체이므로 car2의 값만 변경되게 된다.
아래에서 힙 영역과 스태틱 영역이 어떻게 되는지 살펴보자.
car2를 clone() 메서드를 통해 car1를 복사했더니, 힙 영역에 동일한 값을 가진 새로운 객체가 생성됐다.(주소값도 전부 다름)
결국 값은 동일하지만 완전 독립적인 객체로 사용이 가능하다는 특징이 있다.
그러면 여기서 궁금한 게 방어적 복사는 대체 뭘까?
3. 방어적 복사
생성자로 가변 객체를 받아서 내부 필드를 초기화 하거나, getter 메서드에서 내부 객체를 반환할 때 객체의 복사본을 만들어서 반환하는 것을 말한다.
즉 방어적 복사를 하게 되면 외부에서 가변 객체를 변경해도 인스턴스 내의 가변객체는 변경되지 않는다.
먼저 방어적 복사를 사용하지 않으면 어떻게 되는지 봐보자.
public class Member {
private String name;
public Member(String name) {
this.name = name;
}
}
간단하게 이름을 가지는 Member 클래스
public class VIPMembers {
private List<Member> members;
public VIPMembers(List<Member> members) {
this.members = members;
}
}
Member를 list로 가지는 VIPMembers 클래스
public static void main(String[] args){
List<Member> members = new ArrayList<>();
members.add(new Member("박기현"));
members.add(new Member("김기현"));
VIPMembers vipMembers = new VIPMembers(members);
members.add(new Member("복기현"));
}
그리고 main에서 다음과 같이 진행했다.
1. member 리스트 생성
2. member에 객체 2개 생성.(member = "박기현", "김기현" 가지고 있는 상태)
3. member 리스트로 VIPMembers 생성.
4. member에 객체 1개 생성
자 이렇게 하면 VIPMembers에 있는 member는 변했을까? 한번 출력해보자.
members에 주소값이 3개가 있는 것을 확인할 수 있다.(즉 같이 변경된 것.)
디버그를 통해서도 알아보자.
members의 주소값인 721과 vipMembers안에 있는 members의 주소값이 동일한 것을 알 수 있다.
즉 같은 주소를 가진다는 것.(공유)
이번에는 방어적 복사를 사용해보자.
VIPMembers에서 생성자를 통해 초기화 진행할 때 members를 그대로 받아서 넣는 게 아니라 List를 새로 생성해서 넣어주었다.
public VIPMembers(List<Member> members) {
this.members = new ArrayList<>(members);
}
이 상태로 생성하고 디버깅을 해보자.
members의 주소값과 vipMembers가 가진 members의 주소값이 다른 것을 알 수 있다.
이렇게 변경을 불가능하게 막는 것을 방어적 복사라고 한다.
자 말이 길어졌다.
결국 불변 객체는 한번 생성하고 난 이후에는 내부 상태가 변하지 않는 객체다.
그럼 어떤 이점이 있는가?
1. 우선 Thread-Safe해서 병렬 프로그래밍에 유용하며 동기화를 고려하지 않아도 된다.
멀티 스레드 환경에서는 공유 자원에 대해 서로 변경하다보니 값이 덮어씌워지는 문제가 발생한다.
하지만 불변 객체를 사용하면 내부 값을 변경할 수 없으므로 동기화를 신경쓰지 않아도 된다.
2. 객체의 신뢰성이 높아진다.
값을 변경할 수 없기에 신뢰성이 높아진다는 장점이 있다.(예측 가능)
사실 더 많은 장점들이 있지만 아직 공부해야 하는 부분이 많다.
'면접' 카테고리의 다른 글
백엔드 기술 면접 후기(4) (1) | 2024.01.04 |
---|---|
백엔드 기술 면접 후기(3) (1) | 2024.01.03 |
백엔드 기술 면접 후기(2) (1) | 2024.01.01 |