[!noti]
2024년을 간단하게 회고하면서 현재 매주 공부하고 있는 리팩터링에 대한 주관적인 생각이 담긴 글 입니다.
혹시 제가 리팩터링을 어떻게 공부하고 있는지는 궁금하시다면 링크를 참고해주세요.
레거시라는 큰 산
신규 프로젝트가 시작되었다. 사용자 관리, 게시판 관리 등 admin page를 개발했어야 했는데
기간이 없다는 이유로 PM이 다른 프로젝트의 admin 기능이 포함된 코드를 받아 왔다.
"필요한것만 일부분 수정해서 사용합시다."
그렇게 나는 문서 하나 없이 레거시 코드를 개선 했어야했다.
모르는거 있으면 옆팀에 가서 직접 가서 물어보라고 했다...
무엇이 문제였을까?
먼저 무엇이 필요 없을지 판단을 위해 코드를 읽어 봤다. 진짜 글씨만 읽어 봤다. 무슨 코드인지 알기 정말 힘들었다.
Authentication이라는 class에는 30개가 넘는 맴버 변수가 있었고
특히, API 사용 권한 체크 함수에서는 매개변수 부터가 마음에 안들게 Map<String, Object>으로 되어 있었는데
함수 내부에서 데이터 갱신까지 하고 있었다.
며칠의 우여곡절 끝에 5개의 함수로 나누었다.
그러나 문제는 Map이였다. 이걸 class 객체로 바꾸려고 하니 마지막에 Authentication 객체를 만들기가 너무 어려웠다. Autentication을 나누려고 하니 도통 감이 잡히질 않았다.
그렇게 함수만 나눈 상태로 리팩터링을 끝냈고
리팩터링을 공부해야겠다고 생각한 계기가 되었다.
리팩터링 어떻게 해야할까?
리팩터링에 대한 오해
누군가는 리팩터링에 대해 이런 말을 할 수 도 있다.
- 잘 되는데 왜 리팩터링을 하나요?
- 리팩터링 하다가 오히려 잘 안되면 어떻게 하나요?
- 할일이 많은데 지금 당장 리팩터링을 해야 하나요?
흑백요리사나 냉장고를 부탁해를 보면 요리 중간에 주방을 정리하는 장면들이 나온다.
코드를 고치는 행위 는 요리 중간에 주방을 정리하는 것과 동일하다.
당연히 해야하는 일중에 하나다.
좋은 개발자일 수록 개발하는 중간중간 리팩터링을 진행하면서 좋은 코드를 유지하려 노력한다.
내가 처음 코드를 접했을 때 보다 떠났을 때 더 깔끔하게 하고 나가자
내가 생각하는 리팩터링
[예시 1]: 리팩터링 2판 chapter06 예시: 명령줄 프로그램 쪼개기(자바) 이며 파일에 담긴 주문 개수를 세는 java 프로그램이다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public static void main(String[] args) {
try {
if (args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
String fileName = args[args.length - 1];
File input = Paths.get(fileName).toFile();
ObjectMapper mapper = new ObjectMapper();
Order[] orders = mapper.readValue(input, Order[].class);
if (Stream.of(args).anyMatch(arg -> "-r".equals(arg))) {
System.out.println(Stream.of(orders)
.filter(o -> "ready".equals(o.status()))
.count());
} else System.out.println(orders.length);
} catch(Exception ignore) {}
}
|
cs |
코드가 짧기 때문에 분석하기 쉽긴하지만 무엇을 하는 코드인지 한눈에 들어오지 않는다.
몇달 뒤에 해당 코드를 다시 본다면 또 다시 분석을 해야한다.
이러한 코드들이 많다면 매번 코드를 이해하는데 많은 시간을 소비하게 될 것이다.
의도가 잘 들어나도록 리팩터링을 하자!
해보자 리팩터링
- 테스트 코드 작성
리팩터링 후 컴파일만 잘 되면 과연 잘 된 리팩터링이라고 할 수 있을까?
바로 검증의 과정이 필요하다. 대표적으로 test code를 작성하는 것이다.
리팩터링을 하면서 잘 작동하고 있음을 검증하는 과정은 반드시 필요하다.
- 작동하지 않는 코드는 코드가 아니다.
- 프로그래밍에서 코드는 기본적으로 잘 작동한다를 포함 하고 있다.
- 테스트 코드를 작성하여 잘 작동하는지 검증하자.
* [예시 1] 의 테스트 코드는 가독성을 위해 끝에 올려두었다.
- 함수 추출하기
[예시 1]의 가장 큰 문제는 의도가 들어나지 않는다. 이럴 때 함수를 추출하면 의도가 분명하게 들어나는데
두 가지로 나눌 수 있다.- 주문 목록을 읽는다.
- 주문 목록 동작을 결정한다.
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
|
public static void main(String[] args) {
try {
System.out.println(readOrders(args)); // 주문 목록을 읽는다.
} catch(Exception ignore) {}
}
// 주문 목록을 읽는다.
public static long readOrders(String[] args) throws IOException {
if (args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
String fileName = args[args.length - 1];
return countOrders(args, fileName); // 주문 목록 동작을 결정한다.
}
// 주문 목록 동작을 결정한다.
private static long countOrders(String[] args, String fileName) throws IOException {
File input = Paths.get(fileName).toFile();
ObjectMapper mapper = new ObjectMapper();
Order[] orders = mapper.readValue(input, Order[].class);
if (Stream.of(args).anyMatch(arg -> "-r".equals(arg))) {
return Stream.of(orders)
.filter(o -> "ready".equals(o.status()))
.count();
} else return orders.length;
}
|
cs |
하나의 함수에서 두개의 함수로 분리가 되니 각 함수의 세부 내용만 파악하면 된다.
- 레코드 객체 추출하기
함수의 이름만큼 중요한 것은 매개변수의 이름이다.
여기서 args 이름이 무엇을 하는지 정확하게 들어나지 않는다. 뭐하는 변수지..?
args라는 변수를 의미 있는 변수로 변경하여 의도를 더 명확하게 할 수 있다. 이때 레코드 객체를 만들어 처리하면 편리하다.
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
32
33
34
35
36
37
38
39
40
41
42
43
|
public static void main(String[] args) {
try {
System.out.println(readOrders(args)); // 주문 목록을 읽는다.
} catch(Exception ignore) {}
}
// 주문 목록을 읽는다.
public static long readOrders(String[] args) throws IOException {
CommandLine commandLine = new CommandLine(args); // 레코드 객체 추출
return countOrders(commandLine); // 주문 목록 동작을 결정한다.
}
// 레코드 객체 추출
@Getter
public class CommandLine {
private final String[] args;
public CommandLine(String[] args) {
this.args = args;
if (args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
}
public String filename() {
return this.args[this.args.length - 1];
}
public boolean onlyCountReady() {
return Stream.of(this.args).anyMatch(arg -> "-r".equals(arg));
}
}
// 주문 목록 동작을 결정한다.
private static long countOrders(CommandLine commandLine) throws IOException {
File input = Paths.get(commandLine.filename()).toFile();
ObjectMapper mapper = new ObjectMapper();
Order[] orders = mapper.readValue(input, Order[].class);
if (commandLine.onlyCountReady()) {
return Stream.of(orders)
.filter(o -> "ready".equals(o.status()))
.count();
} else return orders.length;
}
|
cs |
- 클래스 추출하기
일을 바쁘게 하다보면 클래스에 맴버변수와 메서드를 추가할 일이 반드시 생긴다. 그래서 Authentication 클래스의 맴버변수가 30개가 되었을 것이다.(그렇게 추측하고 싶다)
메서드와 데이터가 너무 많은 클래스는 이해하기가 쉽지 않다. 특히 일부 데이터와 메서드를 따로 묶을 수 있다면 분리해야한다.
그래야 다른 사람이 봤을 때 이해하기 쉽다.
함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리한다.
작은 일부의 기능만을 위해 서브클래스를 만들거나, 확장해야 할 기능이 무엇인지 따라 서브클래스를 만드는 방식도 달라진다면 클래스를 나눠야 한다는 신호다.
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
|
public class Person {
private String name;
private final TelephoneNumber telephoneNumber;
public Person(String name, TelephoneNumber telephoneNumber) {
this.name = name;
this.telephoneNumber = telephoneNumber;
}
public String telephoneNumber() {
return this.telephoneNumber.toString();
}
}
public class TelephoneNumber {
private String areaCode;
private String number;
public TelephoneNumber(String areaCode, String number) {
this.areaCode = areaCode;
this.number = number;
}
public String toString() {
return "(" + this.areaCode + ")" + this.number;
}
}
|
cs |
끝으로
요리사가 요리 중간에 설거지와 정리를 하는 것
자동차 정비사가 수리 하면서 수리 도구를 정리하는 것
기타 연주자가 기타 튜닝하는 것처럼
원래 리팩터링은 개발자라면 개발 중간중간에 당연히 해야하는 일이다.
이 당연한 일을 나는 잘 못하고 있었다. 리팩터링에 나중은 없다.
나중으로 미루다가 손을 쓸 수 없는 순간을 직면한다. 고통스러운 일이 될 수 있다.
그 고통스러운 일을 내가 할 확률이 제일 높다. 그러니 잘 하자.
테스트 코드
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
class JavaExampleCodeTest {
@TempDir
Path tempDir;
@Test
void testNoFileNameThrowsException() {
assertThatThrownBy(() -> JavaExampleCode.run(new String[]{}))
.isInstanceOf(RuntimeException.class)
.hasMessage("파일명을 입력하세요.");
}
@Test
void testFileNotFoundThrowsException() {
assertThatThrownBy(() -> JavaExampleCode.run(new String[]{"nonexistent.json"}))
.isInstanceOf(IOException.class);
}
@Test
void testInvalidJsonThrowsException() throws IOException {
File invalidJsonFile = tempDir.resolve("invalid.json").toFile();
try (FileWriter writer = new FileWriter(invalidJsonFile)) {
writer.write("Invalid JSON Content");
}
assertThatThrownBy(() -> JavaExampleCode.run(new String[]{invalidJsonFile.getAbsolutePath()}))
.isInstanceOf(IOException.class);
}
@Test
void testValidJsonWithoutReadyStatus() throws IOException {
File validJsonFile = tempDir.resolve("valid.json").toFile();
try (FileWriter writer = new FileWriter(validJsonFile)) {
writer.write("[{\"status\":\"delivered\"}, {\"status\":\"shipped\"}]");
}
long result = JavaExampleCode.run(new String[]{validJsonFile.getAbsolutePath()});
assertThat(result).isEqualTo(2);
}
@Test
void testValidJsonWithReadyStatus() throws IOException {
File validJsonFile = tempDir.resolve("valid.json").toFile();
try (FileWriter writer = new FileWriter(validJsonFile)) {
writer.write("[{\"status\":\"ready\"}, {\"status\":\"shipped\"}, {\"status\":\"ready\"}]");
}
long result = JavaExampleCode.run(new String[]{"-r", validJsonFile.getAbsolutePath()});
assertThat(result).isEqualTo(2);
}
}
|
cs |
'생각정리' 카테고리의 다른 글
페르소나를 만들어 참여한 개발자 커뮤니티 후기 (0) | 2025.04.05 |
---|---|
할 수 있다는 믿음 (2) | 2025.03.30 |
댓글