티스토리 뷰
비합리적이거나 애매한 가정에 기반해서 코드가 작성되거나 다른 개발자가 잘못된 일을 하는 것을 막지 못할 때 코드는 오용되기 쉽다. 코드를 잘못 사용할 수 있는 몇 가지 일반적인 경우는 다음과 같다.
- 호출하는 쪽에서 잘못된 입력을 제공
- 다른 코드의 부수 효과(입력 매개변수 수정 등)
- 정확한 시간이나 순서에 따라 함수를 호출하지 않음
- 관련 코드에서 가정과 맞지 않게 수정이 이루어짐
위와 같은 상황을 막기 위해선 코드를 오용하기 어렵게 설계하고 작성하는 것이 중요하다.
아래에서 코드를 쉽게 오용할 수 있는 경우를 살펴보고, 오용하기 어렵게 만드는 기법을 알아보자.
불변 객체로 만드는 것을 고려하라
객체를 불변으로 만드는 것이 항상 가능하지도 않고, 또 항상 적절한 것도 아니다. 하지만 가변적인 객체는 코드의 복잡성을 늘리고 문제를 일으킬 수 있기 때문에, 기본적으로는 불변적인 객체를 만들되 필요한 곳에서만 가변적이 되도록 하는 것이 바람직하다.
가변 클래스는 오용하기 쉽다.
클래스를 가변적으로 만드는 가장 일반적인 방법은 세터(setter) 함수를 제공하는 것이다.
아래는 세터 함수로 쉽게 오용될 수 있는 예이다.
class TextOptions {
private Font font;
private Double fontSize;
TextOptions(Font font, Double fontSize){
this.font = font;
this.fontSize = fontSize;
}
void setFont(Font font) { // <- 폰트는 setFont()를 호출해서 언제든지 변경할 수 있다.
this.font = font;
}
void setFontSize(Double fontSize) { // 폰트의 크기는 setFontSize()를 호출해서 언제든지 변경할 수 있다.
this.fontSize = fontSize;
}
Font getFont() {
return this.font;
}
Double getFontSize() {
return this.fontSize;
}
}
만약 다른 개발자에 의해 setFont와 setFontSize가 무작위로 호출되어 값이 변한다면 의도와는 전혀 다른 결과가 나올 것이고, 이를 찾아내기도 쉽지 않다. 이와 같은 상황을 막기 위한 방법으로는 두 가지를 들 수 있다.
객체를 생성할 때만 값을 할당하라
모든 값이 객체의 생성 시에 제공되고 그 이후로는 변경할 수 없도록 함으로써 클래스를 불변적으로 만들 수 있고 오용도 방지할 수 있다. 여기에 final 키워드를 사용한다면 해당 변수를 변경하는 코드를 실수로라도 추가하는 것을 방지할 수 있게 된다.
아래는 세터 함수를 제거하고, 객체 생성시에만 값을 할당하도록 한 예이다.
class TextOptions {
private final Font font;
private final Double fontSize;
TextOptions(Font font, Double fontSize){ // 멤버 변수는 생성 시에만 설정된다.
this.font = font;
this.fontSize = fontSize;
}
Font getFont() {
return this.font;
}
Double getFontSize() {
return this.fontSize;
}
}
불변성에 대한 디자인 패턴을 사용하라
클래스에서 세터 함수를 제거하고 멤버 변수를 final로 표시하면 클래스가 불변적이 되고 버그를 방지할 수 있다. 하지만 만약 일부 값이 반드시 필요하지 않거나 불변적인 클래스의 가변적 버전을 만들어야 한다면, 빌더 패턴을 사용할 수도 있을 것이다.
class TextOptions {
private final Font font;
private final Double fontSize;
TextOptions(Font font, Double fontSize){ // 멤버 변수는 생성 시에만 설정된다.
this.font = font;
this.fontSize = fontSize;
}
Font getFont() {
return this.font;
}
Double getFontSize() {
return this.fontSize;
}
}
class TextOptionsBuilder {
private final Font font;
private Double? fontSize;
TextOptionsBuilder(Font font) { // 빌더는 생성자를 통해 필수 값을 받는다.
this.font = font;
}
TextOptionsBuilder setFontSize(Double fontSize) { // 빌더는 세터 함수를 통해 필수적이지 않은 값을 받는다.
this.fontSize = fontSize;
return this;
}
TextOptionsBuilder build() { // 모든 값이 정해지고 나면 호출하는 쪽에서는 TextOptionsBuilder 객체를 얻기 위해 build를 호출한다.
return new TextOptions(font, fontSize);
}
}
객체를 깊은 수준까지 불변적으로 만드는 것을 고려하라
클래스가 실수로 가변적으로 될 수 있는 일반적인 경우는 깊은 가변성때문인데, 이 문제는 멤버 변수 자체가 가변적인 유형이고, 다른 코드가 멤버 변수에 엑세스할 수 있는 경우에 발생할 수 있다.
class TextOptions {
private final List<Font> fontFamily; // fontFamily는 여러 폰트를 가지고 있는 리스트다.
private final Double fontSize;
TextOptions(List<Font> fontFamily, Double fontSize){
this.fontFamily = fontFamily;
this.fontSize = fontSize;
}
Font getFontFamily() {
return this.fontFamily;
}
Double getFontSize() {
return this.fontSize;
}
}
위 코드가 있을 때, 다른 코드에서 getFontFamily를 호출해서 값을 변경하면 TextOptions 클래스가 참조하는 fontFamily에도 영향을 끼치게 된다.
List<Font> fontFamily = textOptions.getFontFamily();
// textOptions 클래스가 참조하는 리스트와 동일한 리스트를 수정한다.
fontFamily.clear();
fontFamily.add(Font.COMIC_SANS);
실제로 위와 같은 문제는 찾아내기 매우 어렵고 이상한 버그를 일으킬 것이다. 아래는 이를 막기 위한 두 가지 방법이다.
방어적으로 복사하라
클래스가 참조하는 객체가 클래스 외부의 코드에서는 참조할 수 없도록 하면 이 문제를 방지할 수 있는데, 클래스가 생성될 때 게터(getter) 함수를 통해 객체가 반환될 때 객체의 복사본을 만들면 된다.
class TextOptions {
private final List<Font> fontFamily; // 이 클래스만이 참조하고 있는 fontFamily의 복사본
private final Double fontSize;
TextOptions(List<Font> fontFamily, Double fontSize){
this.fontFamily = List.copyOf(fontFamily); // 생성자는 리스트를 복사하고 그 복사본에 대한 참조를 갖는다.
this.fontSize = fontSize;
}
Font getFontFamily() {
return List.copyOf(this.fontFamily); // 리스트의 복사본을 반환한다.
}
Double getFontSize() {
return this.fontSize;
}
}
하지만 위의 경우는 복사하는 데 더 많은 비용이 들 수 있고, 클래스 내부에서 발생하는 변경을 막아주지는 못한다.
불변적 자료구조를 사용하라
불변적 자료구조를 사용한다면, 생성되고 나면 아무도 내용을 변경할 수 없다는 것이다. 이것은 방어적으로 복사본을 만들 필요 없이 객체를 전달할 수 있는 것은 의미한다.
class TextOptions {
private final ImmutableList<Font> fontFamily; // 클래스 내에서도 ImmutableList의 내용을 변경할 수 없다.
private final Double fontSize;
TextOptions(ImmutableList<Font> fontFamily, Double fontSize){
this.fontFamily = fontFamily;
this.fontSize = fontSize;
}
Font getFontFamily() {
return this.fontFamily;
}
Double getFontSize() {
return this.fontSize;
}
}
불변적인 자료구조를 사용하는 것은 클래스가 깊은 불변성을 갖도록 보장하기 위한 좋은 방법 중 하나다. 방어적으로 복사해야 하는 단점을 피하고 실수로라도 클래스 내의 코드에서 변경되지 않도록 보장한다.
요약
- 코드가 오용되기 쉽게 작성되고 나면 어느 시점에선가는 오용될 가능성이 크고 이것은 버그로 이어질 수 있다.
- 코드가 오용되는 몇 가지 일반적인 사례는 다음과 같다.
- 호출하는 쪽에서 잘못된 입력을 제공
- 다른 코드에서 일어나는 부수 효과
- 함수 호출 시점이 잘못되거나 올바른 순서로 호출되지 않는 경우
- 원래의 코드에 연관된 코드를 수정할 때 원래의 코드가 내포한 가정과 어긋나게 수정하는 경우
Reference
톰 롱. 『좋은 코드, 나쁜 코드』. 제이펍, 2022.
'Book > 좋은 코드, 나쁜 코드' 카테고리의 다른 글
[좋은 코드, 나쁜 코드] 가독성 높은 코드를 작성하라 (2) | 2022.12.30 |
---|---|
[좋은 코드, 나쁜 코드] 오류 (0) | 2022.12.25 |
[좋은 코드, 나쁜 코드] 다른 개발자와 코드 계약 (2) | 2022.12.21 |
[좋은 코드, 나쁜 코드] 추상화 계층 (0) | 2022.12.19 |
[좋은 코드, 나쁜 코드] 코드 품질 (0) | 2022.12.18 |
- Total
- Today
- Yesterday
- 구현
- mysql 8.0
- spring boot
- 리팩토링
- 자료구조
- 알고리즘
- Real MySQL
- 정렬
- 데이터베이스
- 스프링부트
- 인프런
- kotlin
- 코테
- 그리디
- webflux
- 스프링
- 파이썬
- 노마드
- 릿코드
- 백준
- 김영한
- Algorithm
- 북클럽
- 문자열
- 노마드코더
- leetcode
- 코틀린
- Spring
- 스프링 부트
- MySQL
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |