이름 짓기는 개발자들이 생각보다 고민을 많이 하는 부분 중 한가지. 간단하지만 개발업무의 중요한 부분들에 강력한 효과가 있기 때문. 코드의 가독성, 유지보수성, 협업 효율성을 모두 고려하여 명명 규칙을 신중하게 적용해야 하며 좋은 이름은 코드의 품질을 높이고, 개발자 간 소통을 원활하게 한다.
의도를 분명히 밝혀라
코드의 이름은 그 역할과 목적을 명확히 드러내야 함. 변수명이나 함수명만 보고도 무엇을 하는 코드인지 쉽게 이해할 수 있어야 한다.
예: calculateTotalPrice()는 총 가격을 계산한다는 의도가 명확. 반면, calc() 같은 이름은 코드의 목적을 모호하게 함
그릇된 정보를 피하라
이름이 잘못된 의미를 전달/혼동 오해를 불러일으킬 수 있음.
account_list라는 변수명이 배열이나 집합을 나타내는 경우 혼란을 초래할 수 있음. 프로그래머에게 List는 특수한 의미이며 account_group이나 bunch_of_account, accounts 등을 사용해야한다 List형으로 사용하더라도 나중에 타입이 변경될수있음.
유사한 개념은 유사한 표기법을 사용하며 이것도 정보이다. 일관성이 떨어지는 표기법은 그릇된 정보임.
의미있게 구분하라
이름이 구체적이어야 같은 프로젝트 내에서 혼동되지 않음.
코드 안에서 같은 의미를 가지는 이름이 비슷비슷하면 혼란을 야기. 이름이 비슷해 보이지만 서로 다른 역할을 한다면, 구체적으로 구분할 수 있도록 해야한다.
잘못된 예: getData()와 getInfo() (차이가 불분명)
올바른 예: getUserData()와 getOrderInfo() (명확한 구분)
이러한 명명 규칙은 코드의 역할을 명확히 하고 일관성을 유지하여 코드베이스 전반에서 가독성을 높이고. 특히 메소드 이름은 행동을 표현하기 때문에 동사로 시작하는 것이 바람직하다. 클래스 이름은 역할을 강조하기 위해 명사로 명명
해법 영역과 문제 영역의 이름 사용
기술적인 부분은 해법 영역의 용어로, 도메인 로직은 문제 영역의 용어로 표현하여 혼동을 줄인다.
(기술적인 코드에는 기술 용어를, 비즈니스 로직에는 도메인 용어를 사용한다는 의미)
예: calculateChecksum() (해법 영역), computeOrderTotal() (문제 영역).
의미 있는 맥락을 추가하라
이름만으로 충분히 의미를 전달할 수 없는 경우, 맥락 정보를 추가하여 더 명확히 표현하면 좋음.
예: address 대신 shippingAddress와 billingAddress로 구분.
불필요한 맥락을 없애라
불필요한 접두어나 맥락을 제거하여 간결하고 명확한 이름을 유지. 클래스 이름이 Product라면 변수명으로 productPrice 대신 price를 사용하는 것이 바람직.
코드 가독성, 협업 효율성 고려. 코드 자체가 스스로 설명할 수 있도록 명명 규칙을 신중하게 고려해야 한다.
검색하기 쉬운 이름을 사용하라
검색이 어려운 이름은 유지보수를 방해. 한 글자 변수명이나 공통적인 이름을 피하고, 코드에서 쉽게 찾을 수 있도록 구체적이고 일관된 이름을 사용.
인코딩을 피하라
의미를 전달하지 않는 접두사나 약어 사용은 가독성을 해침. m, a 같은 인코딩 방식을 피하고, 직관적으로 의미를 전달하는 이름을 사용.
예: m_age 대신 age 사용.
Clean Code에서 함수는 코드의 핵심 구성 요소이다. 프로그램의 기본.
코드의 가독성, 유지보수성, 재사용성을 크게 좌우한다.
public void calculate() {
if (isValid()) {
int result = data * 2;
if (result > 100) {
save(result);
}
}
} 개선된 예public void calculate() {
if (isEligible()) {
saveResult(compute());
}
}public void updateUserAndSendEmail(User user) {
user.update();
email.send(user);
} 개선된 예: public void updateUser(User user) {
user.update();
}
public void sendEmail(User user) {
email.send(user);
}public void renderUser() {
connectToDB(); // 저수준: 데이터베이스 연결
User user = fetchUser(); // 중간 수준: 사용자 데이터 가져오기
user.display(); // 고수준: 사용자 정보 표시
} 개선된 예:public void renderUser() {
User user = retrieveUser(); // 고수준: 사용자 정보 가져오기
display(user); // 고수준: 사용자 정보 표시
//**저수준 세부사항(데이터베이스 연결 등)**은 retrieveUser() 내부로 감추어져 있다.
//함수 자체가 **"사용자를 가져와서 표시한다"**는 고수준 동작을 명확하게 보여줌.
}switch (employee.type) {
case MANAGER:
payManager();
break;
case ENGINEER:
payEngineer();
break;
}개선된 예:employee.pay();서술적인 이름을 사용하라
함수의 이름은 그 역할과 목적을 명확하게 표현해야함. 길어도 괜찮다.
함수 인수
함수 인수는 적을수록 좋습니다. 이상적인 인수 개수는 0개이며, 많아도 2개를 넘지 않는 것이 좋습니다.
public void createUser(String name, int age, String address) {} 개선된 예:public void createUser(UserInfo info) {}public int calculateTotal() {
discount = 0.1; // 부수 효과: 외부 상태를 수정
return price * quantity;
} 개선된 예:public int calculateTotal(double discount) {
return price * quantity * (1 - discount);
}if (setAndCheckStatus()) {
process();
}개선된 예:setStatus();
if (isStatusValid()) {
process();
}if (deleteFile() == -1) {
System.out.println("Error");
}개선된 예:try {
deleteFile();
} catch (IOException e) {
handleError(e);
}chat gpt 문의 - 오류코드를 쓰는 상황은 없는지?
Exception )
가독성 향상: 오류 코드보다 명시적이고 직관적입니다.
코드 관리 용이: 오류 처리와 비즈니스 로직이 명확히 분리됩니다.
일관된 처리: 여러 오류를 한 곳에서 집중적으로 관리할 수 있습니다.
디버깅 용이: 콜 스택 정보를 보존하여 문제 해결이 쉽습니다.
오류코드 )
1. 성능이 중요한 경우
예외는 예외가 발생할 때만 성능에 영향을 미치지만,
예외를 자주 던지고 catch하는 경우, 성능 저하가 발생할 수 있습니다.
예외 처리 과정은 스택 트레이스 추적 등 부가 작업이 많기 때문에,
성능이 중요한 코드 경로에서는 오류 코드를 사용하는 것이 더 효율적입니다.
2.정상흐름의 일부
3.임베디드 시스템 등 제한된 환경
4.단순 유효성 검사

반복하지 마라
중복코드 하지 말 것
구조적 프로그래밍
구조적 프로그래밍은 코드의 흐름을 명확하고 일관되게 작성하는 것을 목표로 함.
잘못된 예 : 복잡한 흐름
public int calculate(int value) {
if (value < 0) return -1;
int result = value * 2;
if (result > 100) return 100;
if (result < 10) return 0;
return result;
}
개선된 예 : 구조적 프로그래밍 적용
public int calculate(int value) {
int result;
if (value < 0) {
result = -1;
} else {
result = value * 2;
if (result > 100) {
result = 100;
} else if (result < 10) {
result = 0;
}
}
return result;
}
chat gpt : 구조적 프로그래밍이 항상 좋은가?
구조적 프로그래밍을 무조건 강제하면 오히려 코드가 불필요하게 길어질 수 있습니다.
💡 예외 상황
함수가 아주 짧고 간단할 때는 중간에 return을 사용하는 것이 더 직관적일 수 있습니다.
예를 들어, 입력값이 잘못된 경우를 초반에 필터링하는 방식은 return을 여러 개 써도 자연스럽습니다.
예시: 간단한 검증 함수
// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))
위의 코드는 아래와 같이 개선될 수 있다
if (employee.isEligibleForFullBenefits())
이렇게 하면 코드 자체로 의도가 명확해져 주석이 필요 없게 된다.
좋은 주석
법적인 주석: 각 소스 파일 첫머리에 들어가는 저작권 정보와 소유권 정보 등은 필요하고 타당
정보를 제공하는 주석: 정규표현식과 같이 복잡한 코드의 의도를 설명하는 주석은 이해를 돕는다.
// kk:mm:ss EEE, MMM dd, yyyy 형식이다.
Pattern timeMatcher = Pattern.compile("\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");
의도를 설명하는 주석: 특정 구현 결정의 이유나 목적을 설명하는 주석은 유용할 수 있다
// 스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
for (int i = 0; i < 2500; i++) {
// ...
}
TODO 주석: 앞으로 해야 할 작업이나 개선 사항을 표시하는 데 사용.
나쁜 주석
객체와 자료구조의 차이점을 명확히 이해하고, 그에 따라 적절히 사용하는 것이 코드의 가독성과 유지보수성을 높이는 데 중요.
자료 추상화
자료 구조를 잘 설계하려면 자료를 추상화하여 내부 구현을 감춰야 함. 자료 추상화는 구조의 세부 사항을 감추고, 인터페이스를 통해 데이터에 접근하는 방법만 제공.
자료/객체 비대칭
자료와 객체는 다른 목적을 갖고 설계되어야 한다.
객체: 데이터를 숨기고, 데이터를 다루는 함수를 외부에 노출.
자료 구조: 데이터를 노출하고, 함수는 별도로 구현.
객체 지향적 설계
public class Rectangle {
private double length;
private double width;
public double area() {
return length * width;
}
}
특징: 객체는 데이터를 감추고, 기능을 제공하여 데이터 중심이 아닌 기능 중심으로 설계.
자료 구조적 설계
public class RectangleData {
public double length;
public double width;
}
public double area(RectangleData rect) {
return rect.length * rect.width;
}
특징: 구조체처럼 데이터를 직접 노출하여 기능과 데이터를 분리.
* 디미터 법칙 : 객체는 자신의 메서드와 필드, 자신이 생성하거나 소유한 객체의 메서드만 호출해야 한다는 규칙
* 기차 충돌
```csharp
person.getAddress().getStreet().getName();
->
//개선
person.getStreetName();
문제점: 메서드 체이닝으로 인해 객체 간 결합도가 높아짐
클린 코드에서 경계(boundary)는 내부 코드와 외부 라이브러리 또는 모듈을 연결하는 지점을 의미. 다. 경계를 잘 관리하면 외부 코드의 변경으로부터 내부 코드를 보호할 수 있어 유지보수성이 높아진다.
외부 코드 사용하기
외부 라이브러리나 모듈을 사용하면 개발 속도가 빨라지고 코드 재사용성이 증가하지만 외부 코드는 언제든지 변경(버전 업데이트, Deprecated)될 수 있으므로, 이를 잘 격리하고 관리하는 것이 중요
직접 연결하지 않고, 중간 어댑터를 사용하여 의존성을 줄여 사용한다.
인터페이스를 통해 간접 접근하여 외부 변화에 유연하게 대응
public interface LogService {
void logInfo(String message);
}
public class Log4jAdapter implements LogService {
private static final Logger logger = Logger.getLogger(MyClass.class);
public void logInfo(String message) {
logger.info(message);
}
}
LogService logService = new Log4jAdapter();
logService.logInfo("Message");
logger의 info 함수가 변경되었을 때, (default tag를 param0에 추가된다던지..)-> logInfo가 사용된 곳 전체가 아니라 logService의 logInfo함수만 고쳐주면 된다.
경계 살피고 익히기
테스트를 통해 경계 파악 필요.
권장사항
아직 존재하지 않는 코드를 사용하기
프로젝트 초기에는 외부 라이브러리가 개발되지 않았거나 도입이 확정되지 않은 경우도 있는데, 인터페이스를 먼저 정의하고 구체적인 구현체는 나중에 추가하는 방식으로 진행
깨끗한 경계
외부 코드와 내부 코드의 경계를 명확히 정의하여 외부 변화로부터 내부 코드를 보호하는 것이 중요. (내부 코드와의 결합도를 최소화)
원칙
클래스 체계
클래스는 작아야 한다 -> 클래스가 작아야 코드 가독성이 높고, 변경에 유연
SRP와 응집도: 잘못된 코드
public class UserManager {
private List<User> users;
private Logger logger;
public void addUser(User user) {
users.add(user);
logger.log("User added: " + user.getName());
}
public void removeUser(User user) {
users.remove(user);
logger.log("User removed: " + user.getName());
}
public void printUsers() {
for (User user : users) {
System.out.println(user.getName());
}
}
}
public class UserManager {
private List<User> users;
public void addUser(User user) {
users.add(user);
}
public void removeUser(User user) {
users.remove(user);
}
public List<User> getUsers() {
return users;
}
}
public class UserLogger {
private Logger logger;
public void logUserAdded(User user) {
logger.log("User added: " + user.getName());
}
public void logUserRemoved(User user) {
logger.log("User removed: " + user.getName());
}
}
응집도를 유지하면 작은 클래스 여럿이 나온다
변경하기 쉬운 클래스
변경으로부터 격리
클래스 설계 시, 변경이 필요한 부분과 그렇지 않은 부분을 분리하여 변경의 영향을 최소화해야 한다.
public class ReportGenerator {
public void generateHTMLReport() {
System.out.println("HTML Report");
}
public void generatePDFReport() {
System.out.println("PDF Report");
}
}
문제점 : 출력 형식이 추가되면 메서드가 계속 늘어남, 새로운 포맷 추가 시 기존 클래스를 수정해야 함
public interface Report {
void generate();
}
public class HTMLReport implements Report {
@Override
public void generate() {
System.out.println("HTML Report");
}
}
public class PDFReport implements Report {
@Override
public void generate() {
System.out.println("PDF Report");
}
}
public class ReportGenerator {
public void generateReport(Report report) {
report.generate();
}
}
개선 -> 새로운 포맷을 추가하려면 Report 인터페이스를 구현한 클래스를 추가하기만 하면 된다.
기존 코드 수정 없이 확장성 확보
if (deletePage(page) == -1) {
System.out.println("Error: Could not delete page.");
}예시: 올바른 방식 (예외 사용)try {
deletePage(page);
} catch (PageDeletionException e) {
System.out.println("Error: " + e.getMessage());
}

Checked 예외는 호출부에서 반드시 처리해야 하므로, 코드가 복잡해진다.
Unchecked 예외(런타임 예외)는 명시적 처리가 필요하지 않아 간결한 코드 작성이 가능하다.
단, 비즈니스 로직에서 꼭 필요한 경우에만 사용해야 한다.
일반적으로 런타임 시점에 발생할 가능성이 높은 예외에 적합하다.
예시: checked 예외 사용
public void process() throws IOException {
// 복잡하고 불필요한 예외 처리
}
예시: unchecked 예외 사용
public void process() {
throw new RuntimeException("Process failed");
}
호출자를 고려해 예외 클래스를 정의하라
오류를 정의할 때 프로그래머에게 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.
잘못된 예시 : 예외 유형이 세분화된 경우
다음은 오류를 형편없이 분류한 사례다.
ACMEPort port = new ACMEPort(12);
try {
port.open();
} catch (DeviceResponseException e) {
reportPortError(e);
logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
reportPortError(e);
logger.log("Unlock exception", e);
} catch (GMXError e) {
reportPortError(e);
logger.log("Device response exception");
} finally {
...
//자원 해제 코드
}
위 문제점
- 중복 처리 코드가 많음(다른 예외들이나 처리 방식이 거의 동일)
위 경우 예외에 대응하는 방식이 예외 유형과 무관하게 거의 동일하다. 그래서 코드를 간결하게 고치기가 쉽다.
- 호출자 입장에서 복잡함
포트 열기라는 단순 작업을 수행하는 코드에 너무 다양한 예외 처리가 들어가 있다.
예외가 다양하니, 호출자 입장에서 어떤 예외가 발생할지 예측하기 어렵다.
올바른 예시: 예외를 래핑하여 단일화
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환.
ACMEPort 클래스가 던지는 예외를 래퍼 클래스로 감싸서 예외 코드를 보다 간결하게 작성할 수 있다.PortDeviceFailure라는 단일 예외로 모든 오류를 처리
여기서 LocalPort 클래스는 단순히 ACMEPort 클래스가 던지는 예외를 잡아 변환하는 래퍼 클래스일 뿐이다.
public class LocalPort {
private ACMEPort acmePort;
public LocalPort(int portNumber) {
acmePort = new ACMEPort(portNumber);
}
public void open() throws PortDeviceFailure {
try {
acmePort.open();
} catch (DeviceResponseException | ATM1212UnlockedException | GMXError e) {
throw new PortDeviceFailure(e); // 예외를 감싸서 던짐
}
}
}
LocalPort 클래스 내부에서 여러 가지 예외를 하나의 예외로 감싼다.
이렇게 하면 저수준 API의 복잡성을 숨기고, 상위 계층에서는 단일화된 예외만 처리할 수 있다.
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch (MealExpensesNotFound e) {
m_total += getMealPerDiem(); // 기본 식비를 추가
}
식비 정보가 없으면 기본값을 사용하는 게 핵심인데,
예외를 던지는 방식은 예외 상황처럼 보이기 때문에 의도가 불명확하다.
올바른 예시
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
public class ExpenseReportDAO {
public MealExpenses getMeals(int employeeId) {
MealExpenses expenses = findMealExpenses(employeeId);
if (expenses == null) {
return new PerDiemMealExpenses(); // 기본 식비 객체 반환
}
return expenses;
}
}
public class PerDiemMealExpenses implements MealExpenses {
@Override
public int getTotal() {
return getMealPerDiem(); // 기본 식비 반환
}
}
특수 사례 패턴 (Special Case Pattern) :
특수 상황을 처리하기 위한 별도 객체를 만들어 코드 흐름을 단순화하는 패턴
null을 반환하지 마라
null을 반환하면 호출자가 매번 null을 체크해야 하며,
하나라도 빠지면 NullPointerException(NPE)가 발생할 가능성이 있다. -> 특수 사례 객체나 빈 컬렉션을 반환하여 null 검사를 방지
잘못된 코드: null을 반환하는 경우
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = persistentStore.getItemRegistry();
if (registry != null) {
Item existing = registry.getItem(item.getId());
if (existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
}
}
문제점
null을 반환함으로써 호출자에게 문제를 떠넘긴다.
null 확인 누락 가능성
불필요한 null 체크 발생
개선 방법 1: 특수 사례 객체 사용
public class NullItemRegistry extends ItemRegistry {
@Override
public Item getItem(String id) {
return new NullItem(); // 특수 사례 객체 반환
}
}
public void registerItem(Item item) {
ItemRegistry registry = persistentStore.getItemRegistry();
Item existing = registry.getItem(item.getId());
if (existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
-> null인지 검사할 필요 없이 항상 Item 타입으로 처리 가능
NullItem에서, 내부의 getBillingPeriod()가 NullBillingPeriod를 반환하여 null이 아님
NullItem의 register() 메서드도 호출 가능하므로 NPE 발생 가능성이 없음
개선 방법 2: 빈 컬렉션 반환
public List<Employee> getEmployees() {
if (/* 직원이 없다면 */) {
return Collections.emptyList(); // 빈 리스트 반환
}
return employeeList;
}
List<Employee> employees = getEmployees();
for (Employee e : employees) {
totalPay += e.getPay();
}
null체크 없이 반복문 바로 사용이 가능하다.
null을 전달하지 마라
null을 인수로 전달하면 NullPointerException(NPE)이 발생 가능.null을 허용하는지 아닌지 불명확하여 코드 사용이 불편. -> 명시적 예외 처리 또는 null 금지 규칙을 적용하여 코드 안정성 확보 필요
잘못된 코드: null을 허용하는 메서드
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
return (p2.x - p1.x) * 1.5;
}
}
// 호출부
MetricsCalculator calculator = new MetricsCalculator();
calculator.xProjection(null, new Point(12, 13)); // NPE 발생
대안) 명시적 예외 던지기
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
if (p1 == null || p2 == null) {
throw new IllegalArgumentException("Invalid argument for MetricsCalculator.xProjection");
}
return (p2.x - p1.x) * 1.5;
}
}
대안) assert 문 사용(단, assert 문은 기본적으로 비활성화되어 있어,프로덕션 환경에서는 오류를 잡지 못할 가능성)
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
assert p1 != null : "p1 should not be null";
assert p2 != null : "p2 should not be null";
return (p2.x - p1.x) * 1.5;
}
}
OCP (개방 폐쇄의 원칙 : Open Close Principle)
유지 보수와 확장이 쉬운 시스템을 만들고자 로버트 마틴이 명명한 객체지향설계 5대 원칙 SOLID중 하나
소프트웨어 구성 요소(컴포넌트, 클래스, 모듈, 함수)는 확장에 대해서는 개방(OPEN)되어야 하지만 변경에 대해서는 폐쇄(CLOSE)되어야 한다
OCP 적용방법-> 상속/컴포지션
클린코드 중 무엇이 중요할까? (chat gpt 문의 : 챕터 구분 및 그 내에서 중요도 순 배치 요청)
이 파트는 클린 코드의 핵심 원칙이자, 코드를 깨끗하고 이해하기 쉽게 만들기 위한 기본기야.
대부분의 클린 코드 요약에서도 가장 강조되는 부분들이야.
코드의 구조적 개선과 유지보수성을 높이는 데 중요한 부분들로, 실무 프로젝트에서 반드시 고려해야 할 내용이다.
클린 코드의 원칙을 유지하고 개선하기 위한 부차적인 요소들이다.
이렇게 나누면 실제 Clean Code를 공부할 때 더 현실적이고, 실무에서도 우선순위가 명확해! 💪🚀