평소 제대로 알고 사용하고 있는 개념이 많지 않다는 사실에 놀랐다. 그동안 프로젝트를 완성하는 것에만 집중하고, 기본기에는 소홀했다는 점을 깨닫고 반성하는 시간을 가질 수 있었다.
안다고 생각했던 것과 몰랐던 것
1. 방어적 복사 - List.copyOf() 와 Collections.unmodifiableList()
이 부분은 글을 하나 새로 써야 할 정도로 양이 많다.
알고 있던 개념
1. 방어적 복사는 복사본을 만들어 반환하는 것이다.
2. List.copyOf() , Collections.unmodifiableList() 를 사용하면, 방어적 복사가 수행되면서, 복사본 List 를 수정할 수 없게 만든다.
몰랐던 개념
지금까지 List.copyOf() 와 Collections.unmodifiableList() 의 차이점을 정확히 알고 있지 않은 상태로 사용하고 있었다.
하지만, 둘 다 불변 List 를 만드는 함수인데 어떤 차이점이 있는 지 궁금했다.
1. List.copyOf() 는 리스트를 벗기고 다시 List 를 씌워 반환하기 때문에, 원본 List 와 복사본 List 자체의 주소는 다르다.
2. Collections.UnmodifiableList() 는 원본 리스트를 변경 불가능한 포장(wrapper) 객체로 감싸서 반환한다. List 를 그대로 java 의 UnmodifiableList 의 생성자로 넘기고, UnmodifiableList 는 생성자로 받은 List 의 참조를 그대로 내부 변수로 저장한다. 따라서, 원본 List 와 복사본 List 자체의 주소가 같다.
정리하자면, List.copyOf() 로 얻은 복사본은 원본 List 와 다른 참조를 가지게 되지만, Collections.UnmodifiableList() 로 얻은 복사본은 원본 List 와 같은 참조를 가진다(포장될 뿐).
그렇다면, List.copyOf() 와 Collections.UnmodifiableList() 는 왜 이렇게 디자인되었고, 어떤 경우에 사용해야 할까?
1. List.copyOf() - 복사본 List 의 주소값은 다르지만, 내부 객체들의 참조는 동일함.
- 복사본 List 의 주소값은 다르지만, 내부 객체들의 참조는 동일함으로써 얻는 이점.
1. 일일히 내부 객체들을 복사할 필요가 없어 메모리를 아낄 수 있고, 복사에 시간을 쓰지 않아도 됨.
- 장점
1. 메모리, 연산 효율성.
2. Collections.UnmodifiableList() 에 비해 원본 List 자체의 변경이 복사본 List 로 전파되지 않음.
- 단점
1. 복사본 List 의 내부 객체의 상태 변경이 원본 List 내부 객체에도 반영(동일한 참조이므로)
- 어떤 경우에 사용해야 할까?
1. 불변 List 를 다루고 싶을 때.
2. 원본 List 가 변경(List.add(), List.remove() 등)될 가능성이 있고, 변경 사항이 복사본 List 에 반영되면 안될 때.
2. Collections.UnmodifiableList() - 복사본 List 의 주소값도 같고, 내부 객체들의 참조도 동일함.
- 복사본 List 의 주소값도 같고, 내부 객체들의 참조도 동일함으로써 얻는 이점
1. 일일히 내부 객체들을 복사할 필요가 없어 메모리를 아낄 수 있고, 복사에 시간을 쓰지 않아도 됨.
2. 새로운 List 를 생성하지 않고, 원본 List 를 포장해 반환하기 때문에, List 생성 비용을 아낄 수 있음 - 하지만, List 생성 자체의 비용은 매우 적음.
- 장점
1. 메모리, 연산 효율성.
2. List 생성(혹은 복사) 비용이 없음.
- 단점
1. 복사본 List 의 내부 객체의 상태 변경이 원본 List 내부 객체에도 반영(동일한 참조이므로)
2. 원본 List의 변경(List.add(), List.remove() 등)이 복사본 List 에도 반영됨.
- 어떤 경우에 사용해야 할까?
1. 불변 List 를 다루고 싶을 때.
2. 원본 List 가 변경(List.add(), List.remove() 등)될 가능성이 있고, 변경 사항이 복사본 List 에 반영되도 상관없을 때.
사용 시 주의점: 둘 다 원본, 복사본 상관없이 List 내부 객체의 수정 사항이 전파되므로, 이를 인지해야 한다.
만약 List 내부 객체의 수정 사항이 전파되지 않게 하려면, 객체를 불변 객체로 만들어야 한다.
정말 불변 객체를 사용하는 방법밖에 없을까?
불변 객체는 비즈니스 로직으로 인해 정해지는 것이 아닌가?
List 내부 객체 상태의 변화 전파를 막기 위해 해당 객체를 불변 객체로 설계하는 것은 비즈니스 로직에 관여하는 것이라 생각한다.
불변 객체를 사용하지 않으려면 복사 시, 원본 List 의 내부 객체들을 새로운 객체들로 복사해 반환하면 된다.
참고: 새로운 객체로 복사하기 위해 복사생성자를 이용하면 좋다. 매개변수가 많으면 코드가 길어지기 때문이다.
코드로 살펴보겠다. position 이 3 이상인 RacingCar 들을 불변 리스트로 반환한다고 가정한다.
public List<RacingCar> 함수명(List<RacingCar> racingCars) {
return new RacingCars(racingCars.stream()
.filter(rC -> rC.getPosition() >= 3)
.toList());
}
복사 시, 원본 List 의 내부 객체들을 새로운 객체들로 복사해 반환하는 방식의 장단점과 사용해야 하는 상황
- 장점
- 1. List 내부 객체의 변경사항이 원본 혹은 복사본 List 내부 객체들에게 전파되지 않는다.
- 단점
- 새로운 객체들을 생성해야 하므로, 메모리와 연산 시간이 많이 든다.
- 어떤 경우에 사용해야 할까?
- 1. 원본 List 가 변경(List.add(), List.remove() 등)될 가능성이 있고, 변경 사항이 복사본 List 에 반영되면 안될 때
- 2. 원본 List 내부 객체의 상태가 변경(setter 등)될 가능성이 있고, 변경 사항이 복사본 List 내부 객체에 반영되면 안될 때.
- 3. 복사본 List 내부 객체의 상태가 변경(setter 등)될 가능성이 있고, 변경 사항이 원본 List 내부 객체에 반영되면 안될 때.
2. equals 비교와 == 비교
알고 있던 개념
1. equals 비교는 객체가 동등한지 비교한다.
2. 클래스의 equals() 를 오버라이딩하면 각 인스턴스를 비교하는 기준을 재정의할 수 있다.
3. equals() 와 hashCode() 를 오버라이딩하면 Collection 내부에서의 처리에 용이하다.
몰랐던 개념
equals 가 객체의 동등성을 비교하므로, equals 가 같으면 완전히 같은 객체라고 생각했다.
하지만, 방어적 복사에 대해 공부하던 과정에서 equals() 가 true 라면 두 객체는 완전히 동일한 것인가에 대한 의문이 들었다.
객체를 비교하는 방법이 두 가지가 있다는 것을 알았다.
객체를 비교하는 법
1. equals() : 객체의 상태가 동일한지 비교한다. → 객체의 동등성을 비교한다.
2. == : 객체의 참조가 동일한지 비교한다. → 객체의 참조를 비교한다.
따라서, equals() 에서 true 가 나온다고 해도, 두 객체가 다른 참조를 가질 수 있기 때문에, 이를 인지하고 사용해야 한다는 것을 깨달았다.
3. gitmessage 와 git hooks
알고 있던 개념
1. .gitmessage.txt 를 통해 git commit 시 템플릿을 적용할 수 있다.
몰랐던 개념
어딘가에서 .gitmessage.txt 를 프로젝트의 root 경로에 만든 후,
git config --global commit.template .gitmessage.txt
해당 명령어를 실행하면 commit 시 템플릿을 통해 커밋 메시지를 작성할 수 있다고 들었다.
나는 이 정보를 얻은 후, 어떻게 동작하는지에 대한 생각은 하지 않은 채 그대로 사용하기만 했고, 이번 프리코스에서도 위 방법을 사용해 커밋 메시지를 일관되게 작성하려고 했다.
하지만, 프리코스 목표 중 하나인 “내가 안다고 생각하는 것이 있다면, 스스로에게 꼬리질문해서 정말 아는지 확인한다” 를 하는 과정에서 위 방식에 대해 제대로 알고 있지 않다는 생각이 들었다.
이를 해결하기 위해, 우선 명령어를 분석해보았다.
1. git config : git 의 환경 설정을 하는 명령어이다.
2. --global: 설정 레벨 중, global 설정을 한다는 옵션이다.
- 설정 레벨은 다음과 같다.
1. system : 시스템 전역의 모든 사용자에게 적용된다. 운영 체제 레벨로 적용된다.
2. global : 현재 사용자에게만 적용된다. 현재 사용자의 모든 로컬 레포지토리에 적용된다.
3. local : 현재 레포지토리에만 적용된다.
- git 은 설정 레벨 간의 충돌이 있을 경우, local → global → system 순으로 설정을 적용한다.
- 만약 local 에 a 설정을 true 로 하고, global 에 a 설정을 false 로 하면 local 에서 작업 시 a 설정이 true 로 동작한다.
3. commit.template : 커밋 시 사용할 템플릿을 설정한다.
- git commit 시 해당 템플릿이 사용되고, 해당 템플릿을 기반으로 커밋 메시지를 작성할 수 있다.
4. .gitimessage.txt : 커밋 템플릿으로 어떤 파일을 명시할지 정한다.
- 반드시 .gitmessage.txt 가 아니어도 된다는 사실을 알았다. 어떤 확장자의 어떤 파일이던지 올 수 있다.
- commit 하는 레포지토리의 .git 파일이 있는 디렉터리 기준으로 파일을 찾는다. 만약 .git 이 b 디렉터리에 있고, /a/commit.txt 로 설정한다면, commit 시 b/a/commit.txt 파일을 찾을 것이다.
그렇다면, commit 메시지 템플릿을 적용하는 방법이 commit.template 설정밖에 없는 걸까?
AngularJS 의 커밋 메시지를 적용하기 위해 AngularJS 의 깃허브 레포지토리에서 git hooks 를 사용해 커밋 메시지 템플릿을 자동으로 설정하라는 가이드를 발견했는데, prepare-commit-msg 파일을 .git/hooks 에 넣으면 동작한다고 했다.
git hooks 와 prepare-commit-msg 에 대해 조사해보았다.
1. git hooks
- git hooks 란, git 이벤트가 발생할 때 shell script 를 실행하게 해 주는 기능으로, 특정 이벤트들에 대한 shell script 를 정의해 .git/hooks 에 해당 형식의 파일명으로 넣어 놓으면 그 이벤트가 발생했을 때, shell script 가 실행된다.
- 여러 가지 hooks 들이 있지만, 너무 많기 때문에 내가 알고자 하는 prepare-commit-msg 만 알아보았다.
2. prepare-commit-msg
- 실행 시점: git commit 을 실행한 후, 편집기가 실행되기 전.
- 용도: commit 메세지에 Templete 적용 등
이를 토대로, prepare-commit-msg 에서 commit template 을 적용할 수 있다는 것을 알았다.
echo 명령어를 통해 편집기에 template 을 설정할 수 있다.
4. 자바의 자동 형 변환 (String + Integer = String?
알고 있던 개념
1. 자바는 여러 가지 자동 형 변환을 제공한다.
ex) 객체를 문자열로 변환하면, 해당 객체의 toString() 이 동작한다.
몰랐던 개념
테스트 코드를 작성하며 깜박하고 Integer 값을 String 으로 변환하지 않았다. 테스트가 모두 통과한 후, 이것을 알게 되었고, 이상하다는 생각이 들었다.
String tryCount = Integer.MAX_VALUE + “1”;
위 코드를 작성했었는데, 컴파일 오류가 발생하지 않았고, 값도 정상적으로 설정되고 있었다.
여러 가지 상황들을 테스트해보며 자바는 Number 타입과 String 타입의 간의 연산 시 String 타입으로의 형 변환을 해 준다는 것을 알았다.
고민했던 점들과 시간을 많이 쏟았던 부분들 + 의사결정의 근거
1. MVC 에서의 Controller 의 역할
MVC에서 Controller는 사용자의 요청을 받아 이를 처리하고, View를 통해 사용자에게 결과를 보여주는 역할을 한다.
기존에는 이 역할을 충분히 이해하지 못한 채, Controller에 run() 메서드만 제공하는 방식으로 프로그래밍하고 있었다. 하지만 run() 메서드가 모든 비즈니스 로직을 담당하는 것을 보고, 해당 메서드가 너무 많은 책임을 지고 있다고 판단했다.
이전 코드 예시(Application)
main(){
Controller controller;
controller.run();
}
Controller는 사용자와 상호작용하는 역할을 가지고 있는데, run() 메서드 하나로 이를 전부 처리하는 것은 적절하지 않다고 보았다.
따라서, Controller는 추상화된 비즈니스 로직 수행 메서드를 외부에 노출하고, Application에서 Controller의 추상화된 메서드를 호출하도록 설계했다. 이를 통해 Controller가 사용자와의 상호작용을 더욱 명확하게 담당할 수 있게 되었다.
수정 후 코드 예시(Application)
main(){
Controller controller;
controller.setUpRacingCars();
controller.setUpTryCount();
controller.playRacing();
controller.printRacingResult();
}
2. 자동차의 재사용을 막기 - 깊은 복사
RacingCar 객체들을 관리하는 일급 컬렉션인 RacingCars를 만들었다. 또한, 지금은 RacingCars를 재사용할 일이 없지만, 미래에는 재사용할 가능성이 있다는 것을 가정했다.
문제는 특정 라운드를 진행할 때 사용한 RacingCars를 그대로 재사용한다면, 내부의 RacingCar들의 상태가 변경될 경우 그 변화가 이후 라운드에 영향을 미칠 수 있다는 점이다.
예를 들어, 리스트의 방어적 복사를 하더라도 RacingCar 객체 자체는 그대로 참조되기 때문에, 복사본의 RacingCar 상태가 바뀌면 원본에도 영향을 주게 된다.
이를 막기 위해 RacingCars 의 생성자에 깊은 복사를 적용해 새로운 RacingCars 가 생성될 때, 모든 RacingCar 의 참조를 복사해 새로운 객체들을 만들도록 디자인했다.
이 방법의 장점과 단점은 다음과 같다.
- 장점
- 복사본의 변경이 원본에 영향을 미치지 않으므로, 독립적인 상태를 유지할 수 있다.
- 단점
- 객체를 전부 복사하게 되므로, 객체의 수가 많아질수록 메모리 비용과 연산 비용이 발생한다.
- 복사 후, 한 번만 사용하고 더 이상 사용하지 않는 일이 많아지면, 불필요한 객체가 쌓여 GC가 자주 발생하게 되어 성능 저하로 이어질 수 있다.
나는 메모리와 연산 비용을 소모하더라도 RacingCar 객체의 독립적인 상태를 유지하는 것이 올바르다고 생각했다. RacingCars 를 재사용하게 된다면, 복사된 객체는 한번 쓰고 버리는 상황보다는 계속해서 사용할 가능성이 높다고 판단했기 때문이다.
3. 일급 컬렉션의 역할과 일급 컬렉션의 테스트
배경: RacingCar 객체의 일급 컬렉션인 RacingCars 를 만들었다.
나는 일급 컬렉션이 Collection을 래핑하여 외부에 여러 API(함수)를 제공하는 역할을 한다고 생각한다. 따라서 일급 컬렉션에 getter나 setter가 있다면, 그 역할에 위배된다고 생각한다. 내부 컬렉션을 외부에서 직접 다루게 되면 Collection을 포장하는 의미가 없어지기 때문이다.
하지만 일급 컬렉션을 테스트하는 과정에서 내부 컬렉션의 값이 실제로 변경되었는지 확인하기 어려운 문제를 발견했다. 이를 해결하기 위해 세 가지 방법을 고려했다.
- 1. Adaptor 디자인 패턴을 통한 RacingCars 기능 확장
- 테스트 용도의 RacingCarsAdaptor를 만들어 getter와 setter를 선언하는 방법이다. RacingCars를 상속받아 내부 변수로 List<RacingCar> 를 가지도록 해 RacingCars의 Collection 값을 확인할 수 있다.
- 장점: RacingCars를 수정할 필요가 없다.
- 단점: 코드가 많아져 프로그램 규모가 커질수록 복잡도가 올라간다.
- 2. Java Reflection을 사용해 내부 Collection의 값 가져오기
- 장점: RacingCars를 수정할 필요가 없다.
- 단점 1: 캡슐화 원칙을 위반하는 것이기 때문에, RacingCars의 코드 변경 시 Reflection 코드도 함께 변경해야 한다.
- 단점 2: Reflection 사용 시 문제 발생 시 원인 파악이 어려울 수 있고, 잘못된 타입 캐스팅 등의 문제가 발생할 수 있다.
- 3. RacingCars에 getter와 setter 만들기
- 장점: 매우 간단하다.
- 단점 1: 일급 컬렉션의 역할에 위배된다.
- 단점 2: 잘못된 사용 가능성이 있다.
- 단점 3: 협업 시 함수의 의도를 파악하기 어려울 수 있다.
Adaptor를 통한 RacingCars 확장을 고려했으나, 현재 RacingCars의 생성자가 깊은 복사를 수행하도록 했기 때문에 내부 Collection의 객체들과 원래 Collection의 객체들이 다른 참조를 가지게 된다. 참조가 다르므로 비교할 수 없어 Adaptor는 사용할 수 없었다.
RacingCars에 getter와 setter를 추가하는 방식은 일급 컬렉션의 역할에 위배된다고 생각해 선택하지 않았고, Java Reflection을 사용해 내부 Collection의 값을 가져오는 방식을 선택했다.
'우아한테크코스 7기 프리코스' 카테고리의 다른 글
7기 프리코스 4주차 회고 (KPT 회고) (1) | 2024.11.12 |
---|---|
7기 프리코스 3주차 회고 (KPT 회고) (7) | 2024.11.05 |
7기 프리코스 1주차 회고(알고 있다고 생각했던 것들) (1) | 2024.10.23 |