[새배내] 체스 미션을 하며 새로 배운 내용

Junseo Kim·2021년 3월 18일
0

[우아한테크코스3기]

목록 보기
13/27

2021.03.16 ~ 2021.04.02


변수명 추천 사이트

Curioustore

페어 코드 가져오는 방법2

git remote add 원하는별명 원하는원격저장소주소 // 코드를 가져오고자 하는 저장소를 등록한다.

git fetch 등록한별명 원하는브랜치이름 // 등록한 원격 저장소에서 가져오고 싶은 브랜치를 fetch한다.(fetch한 경우 로컬에 반영되는 것이 아니라 가상공간에 가져왔다고 생각)

git rebase 등록한별명/브랜치이름 // fetch 해놓은 것을 로컬에 반영한다.

git push origin master // 내 원격 저장소에 반영한다.

unmodifiableList vs new ArrayList

unmodifiableList()는 read-only 용도로 사용하기 위한 것으로 add등의 수정을 할 수 없게 해준다. 하지만 원본 리스트 자체가 수정되지 않도록 보장해주지는 않는다.

아래와 같이 값을 꺼낼 때 Collections.unmodifiableList()를 사용해서 리스트를 꺼낸다고 해보자.

public class Strings {
    private final List<String> strings;

    public Strings(List<String> strings) {
        this.strings = new ArrayList<>(strings);
    }

    public void add(String string) {
        strings.add(string);
    }

    public List<String> strings() {
        return Collections.unmodifiableList(strings);
    }
}

아래의 출력 결과가 어떻게 될까?

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("1");
    list.add("2");
    list.add("3");

    Strings strings = new Strings(list);

    List<String> unmodifiableList = strings.strings();
    System.out.println(unmodifiableList);
    strings.add("4");
    System.out.println(unmodifiableList);
}

unmodifiableList()가 연결을 끊어줬다면 첫 번째 출력문과, 두 번째 출력문 모두 [1, 2, 3] 이 나와야한다. 하지만 돌려보면 두 번째 출력문은 [1, 2, 3, 4]가 출력된다. unmodifiableList()가 원본 리스트 자체의 불변을 보장하지 않기 때문이다.

Strings의 strings()를 아래와 같이 변경해보자.

public List<String> strings() {
    return new ArrayList<>(strings);
}

이 후 다시 돌려보면 두 출력문 모두 [1, 2, 3]이 출력되는 것을 볼 수 있다. 결론적으로 unmodifiablelist는 원본과의 연결을 끊어주는 역할은 하지못한다.

인자 값으로 Null 허용하지 않기

원시값과 문자열을 포장해서 사용하면 인자 값으로 null을 허용하지 않을 수 있다.(객체 생성 시 검증을 할 수 있기 때문에)

final 클래스로 상속을 막을 경우 인터페이스 활용

final 클래스로 만들면서 테스트 가능하게 하려면 인터페이스로 추출하는 방법이 있다.

final 클래스는 상속이 불가능하기 때문에 메서드를 재정의 할 수 없다.(익명 클래스는 부모 클래스를 상속받는 클래스이다.)

public class Car {
    ...
    public void move(RandomNumber randomNumber) {
        if (randomNumber.movable()) {
            this.position = position + 1;
        }
    }
}
public final class RandomNumber {
    private static final int FORWARD_NUMBER = 4;

    private final int randomNumber;

    public RandomNumber(int randomNumber) {
        this.randomNumber = randomNumber;
    }

    public boolean movable() {
        return randomNumber >= FORWARD_NUMBER;
    }
}
// RandomNumber가 final 클래스이기 때문에 익명 클래스로 만들 수 없다.
car.move(new RandomNumber(4) {
    @Override
    public boolean movable() {
        return true;
    }
});

이런 경우 인터페이스를 활용할 수 있다.

// 재정의 할 메서드를 인터페이스로 분리하고
public interface Number {
    boolean movable();
}
// 인터페이스를 implements 해서 메서드를 구현한다.
public final class RandomNumber implements Number {
    private static final int FORWARD_NUMBER = 4;

    private final int randomNumber;

    public RandomNumber(int randomNumber) {
        this.randomNumber = randomNumber;
    }

    public boolean movable() {
        return randomNumber >= FORWARD_NUMBER;
    }
}
// 해당 메서드를 사용하는 곳의 인자를 인터페이스 타입으로 바꿔준다.
public class Car {
    ...
    public void move(Number number) {
        if (number.movable()) {
            this.position = position + 1;
        }
    }
}
// 필요할때마다 인터페이스를 구현해서 넣어줄 수 있다.
car.move((new Number() {
    @Override
    public boolean movable() {
        return true;
    }
}));

// 람다식으로 사용할 수도 있다.
car.move(() -> true);
car.move(() -> false);

인터페이스 중복

인터페이스의 구현체가 늘어나면 중복이 발생할 수 밖에 없다.

car (interface)
Sonata, Avante(car 구현체)

중복을 제거하려면 car 인터페이스를 구현하는 추상 클래스를 정의해 줄 수 있다. 이렇게 하면 기존 Sonata, Avante에서 발생한 중복을 abstractCar에서 처리해 줄 수 있다.

car (interface)
abstractCar (car를 구현하는 abstract class)
Sonata, Avante(구현체)

궁금증: 저렇게 추상 클래스를 만들어주면 가장 상단에 인터페이스가 존재해야하는 이유가 있나? 인터페이스의 명세들을 추상 클래스 내부에 abstract 메서드로 만들면 되는 것 아닌가?
-> 추상 클래스가 하나만 있으면 그렇게 생각할 수 있지만 여러개가 있는 경우 중복되는 부분만 중간 추상 클래스에서 구현해줄 수 있기 때문에

인터페이스의 디폴트 메서드
인터페이스의 디폴트 메서드를 만들어서 중복을 처리하는 방법도 있지만, 만약 필드를 사용해야하는 경우는 디폴트 메서드로 처리할 수 없다.(인터페이스는 필드를 가질 수 없기 때문)

추상클래스나 부모클래스의 접근제한자

클래스의 인스턴스 변수를 자식 클래스에서 접근하기 위해 protected로 오픈하는 경우가 있지만 private으로 구현하는 것을 권장. 접근이 필요하다면 메서드를 만들어서 접근하도록.(protected는 캡슐화 관점에서 좋은 접근 방법은 아니다.)

protected는 메서드에는 사용할 수 있다.

인스턴스 변수는 가능하면 private으로 두기.

클래스 명

무엇을 하는지가 아닌 무엇인지에 기반해 이름을 지어야 한다. 무엇인지에 기반하면 객체를 좀 더 자립적인 존재로 볼 수 있게 된다.(스스로 결정하고 행동)

무엇을 하는지로 이름을 지으면 수동적이 될 수 있다. -> 절차 지향적 프로그래밍 유도

무엇을 하는지는 메서드이름으로 나타낼 수 있다고 생각하며 무엇인지 안에 포함되는 개념이라고 생각한다.

객체 협력 설계
메세지를 정하고 객체의 협력을 구상한다. 무엇을 하는지 생각해서 역할을 수행할 객체를 생각하고, 해당 객체가 가진 책임들을 보고 해당 객체가 무엇인지를 생각해봐서 이름을 짓는다.

무엇을 하는지로 이름을 지은 예시: CashFormatter
무엇인지로 이름을 지은 예시: FormattedCash or Cash

-er 로 끝나는 이름을 쓰지 않기

메서드 명

빌더

뭔가를 만들고 새로운 객체를 반환하는 메서드. 반환 타입은 절대 void가 될 수 없다.
이름은 명사로 짓는다. 형용사 + 명사도 가능

// 예시
int pow(int base, int power);
float speed();
Employee employee(int id);
String parsedCell(int x, int y);

아래의 예시의 두 메서드는 Bakery를 자율적인 객체로 존중하지 않고 하나하나 명령하는 느낌이다.

class Bakery {
    ...
    public Food cookBrownie() {
        ...
    }
    
    public Drink brewCupOfCoffee(String flavor) {
        ...
    }
}

이렇게 이름을 바꿔주면 내부에서 어떻게 처리하든 brownie와 coffe를 가져오라는 느낌이 든다. Bakery가 알아서 하도록 존중

class Bakery {
    ...
    public Food brownie() {
        ...
    }
    
    public Drink coffee(String flavor) {
        ...
    }
}

조정자

객체로 추상화한 실세계 엔티티를 수정하는 메서드. 항상 void를 반환한다.
이름은 동사로 짓는다. 부사 + 동사도 가능

// 예시
void save(String content);
void put(String key, Float value);
void remove(Employee emp);
void quicklyPrint(int id);

바텐더에게 음악을 틀어 달라고 요청하는 상황이라고 생각해보자.
1. 음악을 틀어주세요.void play()
2. 음악을 틀고, 현재의 볼륨 상태를 말해주세요. Volume play()

2번 같은 경우 바텐더에게 하나하나 명령하는 느낌이다. 1번 같은 경우는 요청만 하고 나머지는 바텐더가 알아서 하는 느낌이다.(자율적인 존재로 존중)

빌더와 조정자가 혼합된 경우

아래의 경우 write라는 이름은 조정자를 뜻하지만 반환 값이 int이기 때문에 빌더의 반환값이다.

class Document {
    ...
    int write(InputStream content) {
    ...
    }
}

이렇게 빌더와 조정자가 혼합된 경우 아래와 같이 클래스를 추출하여 각 메서드를 따로 분리할 수 있다.

class Document {
    ...
    OutputPipe output() {
    ...
    }
}

class OutputPipe {
    ...
    void write(InputStream content) {
        ...
    }
    
    int bytes() {
        ...
    }
    
    long time() {
        ...
    }
}

예외

boolean 반환값을 반환하는 경우는 void를 반환하는 것이 아니므로 빌더이지만 가독성 측면에서 이름을 형용사로 짓는다.

boolean empty();
boolean readable();
boolean negative();

클래스의 생성자와 메서드의 수

생성자의 수는 5개 ~ 10개를 추천한다.(주 생성자 1개, 나머지는 부 생성자) -> 유연성이 향상 될 수 있다. 사용자도 편해진다.

public 메서드의 수는 2개 ~ 3개를 추천한다.(getter, equals, hashcode, tostring 등 기본 메서드 제외) -> public 메서드 수가 많아지면 클래스의 초점을 흐리고 SRP 위반 가능성이 올라간다.

=> 응집도가 높아진다.

생성자

생성자는 코드가 없어야하고, 오직 할당문만 포함해야한다.

좋은 주생성자

클래스의 인스턴스 변수와 같은 타입을 가지는 생성자. 할당만 해준다.

생성자 순서 컨벤션

부 생성자 (부 생성자는 인자 작은 것 부터)

주 생성자

생성자에 코드를 넣지 않기

"fortune, air, curry" 등의 문자열을 생성자로 넘겨서 생성자 내부에서 처리(가공)해도 될까?

생성하는 로직이 복잡하다면 로직을 수행하는 느낌이기 때문에 외부에서 처리 후 넘겨준다.(view에서 처리하거나, 중간계층에서 처리)

생성자에 코드를 넣지 말라는 것은 인자에 손대지 말라는 소리이다.

public class Cash {
    private int dollars;
    
    public Cash(String dollar) {
        this.dollars = Integer.parseInt(dollar); // 인자에 손을 댄 경우
    }
}

외부에서 처리하지 않고 인자에 직접 손을 대지 않으려면 클래스를 생성해서 처리할 수도 있다.

public class Cash {
    private Number dollars;
    
    public Cash(String dollar) {
        this.dollars = new CachedNumber(new StringAsInteger(dollar));
    }
}
public class StringAsInteger extends Number {
    private String source;
    
    public StringAsInteger(String source) {
        this.source = source;
    }
    
    @Override
    public int intValue() {
        return Integer.parseInt(this.source);
    }
}
public class CachedNumber extends Number {
    private Number origin;
    
    private List<Integer> cached = new ArrayList<>(1);
    
    public CachedNumber(Number num) {
        this.origin = num;
    }
    
    @Override
    public int intValue() {
        if (this.cached.isEmpty()) {
            this.cached.add(this.origin.intValue());
        }
        return this.cached.get(0);
    }
}

이런식으로 리팩토링하면 실제 사용하는 시점까지 객체 변환 작업이 연기된다. 또 생성자에서는 할당 작업만 일어나게 되므로 성능 최적화가 쉬워져 실행속도가 빨라진다. 요청을 받을 때만 행동하고 그 전에는 아무 일도 하지 않는다.(lazy)

생성자에서 여러 new가 쓰이는 것은 좋은 구조이다.

테스트 코드도 프로덕션 코드의 일부

테스트 코드도 중복제거 등 리팩토링을 해야한다.

Stable sort & Unstable sort

sorting 시 같은 key를 가진 요소들이 sorting 후에 순서가 바뀌게되면 unstable sort이다. 반대로 sorting 시 같은 key를 가진 요소들의 순서가 바뀌지 않으면 stable sort이다.

Stable sort

  • bubble sort
  • insertion sort
  • merge Sort

Unstable sort

  • selection sort
  • heap sort
  • quick Sort

참고
stable & unstable sort에 대하여

Collections.sort vs Arrays.sort

Collections.sort와 Arrays.sort의 차이점이 뭘까?

Arrays.sort는 내부적으로 primitive type 배열이 들어오면 dual-pivot quicksort를 수행한다. 하지만 Object type 배열이 들어오면 timSort를 수행한다. 들어오는 type에 따라 수행되는 sort가 달라지는 것이다.

Collections.sort는 내부적으로 Arrays.sort를 호출한다. 여기서 중요한 것은 Collection은 Object type으로만 만들 수 있기 때문에 Collections.sort는 항상 Arrays.sort에 Object type 배열로 넘어가게 되므로 timSort를 호출하게 되는 것이다.

Rebase vs merge

rebase: branch의 base를 옮기는 것. base를 재배치한다.
merge: branch를 통합하는 것.

(추후에 직접 테스트해보고 정리)

테스트하기 편한 코드 만들기

테스트하기 편한 코드로 만들다보면 코드가 유연해진다.

의존성 주입

의존성 주입

instanceof 사용하지 않기

instanceof는 사용하지 않는 것이 좋다. instanceof를 사용하는 것은 상위 타입만을 이용해서 프로그래밍을 할 수 없다는 뜻이다. 즉, 하위 타입이 상위 타입을 대체할 수 없으므로 새로운 하위 타입이 추가될때마다 코드를 수정해야할 가능성이 높아진다.

instanceof는 다형성(상속, 인터페이스)으로 해결가능하다.

상태를 가지지 않는 객체

상태를 가지는 객체는 매번 인스턴스를 만들어 줘야하지만, 상태를 안가지는 객체는 인스턴스 하나만 있어도 된다.(메모리 낭비와 성능상 문제 때문에)

데이터 중복

데이터 중복이 코드 중복보다 더 좋지 않다. 항상 데이터 별로 싱크를 맞춰줘야하기 때문에.

public class Cars {
    private final List<Car> cars;
    private int carSize;
    ...
}

carSize는 cars와 계속 싱크를 맞춰줘야한다. 불필요한 필드이다.

인터페이스 추출

서로 다른 패키지간의 의존관계가 존재할 경우 인터페이스로 분리하는 것을 추천한다.

만약 car 패키지 내부의 Car 클래스가 number 패키지의 RandomNumber(일반 클래스)를 필드로 가지고 있는 경우, RandomNumber를 인터페이스로 추출하고 추출한 인터페이스 타입을 Car 클래스가 가지게 한다.

참고
MS의 C# 스타일

외부 패키지에서 사용하지 않는 클래스

다른 패키지에서 사용하지 않는 클래스는 꼭 public일 필요가 없다. default로 바꿔주어도 된다.

강한 결합과 느슨한 결합

결합(coupling)이란 서로 상호작용하는 시스템들간의 의존성을 의미한다. 강한 결합은 다른 객체에 대한 많은 정보를 필요로하며, 두 객체가 서로 강한 의존성을 가지는 것이다. 이런 경우 하나의 객체의 변경은 결합되어있는 다른 객체의 변경도 유발한다. 따라서 가능하면 느슨한 결합을 가지도록 바꿔주는 것이 좋다. 느슨한 결합은 두 객체가 상호 작용을 하긴 하지만 서로에 대해 잘 모르는 것이다.

참고
Coupling이란? Tight Coupling vs Loose Coupling 정리

컨트롤러의 상태

컨트롤러는 상태를 가지지 않아야한다. 상태가 없어야지 컨트롤러 하나를 여러 플레이러가 공유할 수 있게 된다. 컨트롤러가 상태를 가지고 있다면 여러 플레이어가 해당 상태에 동시에 접근할 경우 문제가 발생할 수 있기 때문이다.

커스텀 예외의 장점

  • 동일한 예외를 던질 때 예외 메세지 중복을 방지할 수 있다.
  • 예외 종류에 따라 HTTP 상태 코드를 다르게 주거나 할 수 있다.

추상 메서드 위치

추상 메서드는 가장 하단에 두는 것이 헷갈리지 않는다.

boolean 변수 이름 짓기

boolean을 리턴하는 메서드는 validate와 같은 네이밍을 하지 않는다.(validate로 시작하는 메서드는 검증을 하는 메서드로, 값의 유효성을 검사하고 예외를 던진다.)

참고
Bool 변수 이름 제대로 짓기 위한 최소한의 영어 문법

커맨드 패턴

Command(명령, 요구사항, 행위)를 객체로 캡슐화하는 패턴. Command 인터페이스를 만들고, 필요한 명령어 클래스를 해당 인터페이스를 구현하여 생성한다. 그 후 Command 인터페이스 타입의 메서드를 사용하여 각 구현체의 실제 구현을 호출한다.(전략 패턴과 유사한 것 같다는 느낌 🤔)

DAO(Data Access Object)

DB의 데이터에 접근하기 위해 생성하는 객체. DB에 접근하기 위한 로직과 비즈니스 로직을 분리하기 위해 사용한다. DB에 접속하여 데이터의 CRUD를 실제로 수행하는 클래스이다.

PreparedStatement

statement를 상속받는 인터페이스로 SQL문을 실행시키는 객체. 객체 생성시 넘겨주는 SQL문으로만 사용가능하며 재사용 할 수 없다. 변수는 ?로 표시해주고 LIKE 키워드는 사용할 수 없다.

public void addUser(User user) throws SQLException {
    String query = "INSERT INTO user VALUES (?, ?)";
    PreparedStatement pstmt = getConnection().prepareStatement(query);
    pstmt.setString(1, user.getUserId());
    pstmt.setString(2, user.getName());
    pstmt.executeUpdate();
}

ResultSet

SELECT의 결과 데이터를 저장하는 객체이다. 데이터를 한 행 단위로 불러올 수 있다. column의 값을 특정 타입으로 지정해서 가져올 수 있다.

public User findByUserId(String userId) throws SQLException {
    String query = "SELECT * FROM user WHERE user_id = ?";
    PreparedStatement pstmt = getConnection().prepareStatement(query);
    pstmt.setString(1, userId);
    ResultSet rs = pstmt.executeQuery();

    if (!rs.next()) return null;

    return new User(
            rs.getString("user_id"),
            rs.getString("name"));
}

검증 메서드

검증 메서드는 보통 validates~의 이름을 가진다. 또한 반환값이 있을 수도 없을 수도 있는데, 취향이나 팀 컨벤션에 따르도록 하자.

단 하나의 Connection 객체

public static final Connection CONNECTION = getConnection();

이런식으로 단 하나의 Connection 객체를 만들어 사용한다면 Thread safe 하지 못하다
사용자들이 보낸 요청 간에 연결이 공유되므로 모든 쿼리가 서로 간섭받게 된다.

또한 Connection을 애플리케이션이 돌아가고 있는 동안 계속 열어둔다면 resource leaking 문제도 발생한다. 따라서 보통 30분 ~ 8시간 정도 연결되있었다면 Connection이 회수된다.(static final이 아닌 인스턴스 변수여도 마찬가지)

따라서 항상 가능한 최단 범위에서 연결하고 닫아줘야한다. try-with-resources를 사용하자

참고

DB 테스트 코드

DB관련된 테스트를 할때는 실제 DB서버가 아닌 테스트용 DB를 사용한다. 또는 전략패턴을 사용한 테스트를 하듯이(프로덕션 코드와 테스트 코드에 각기 다른 전략을 넣어서 테스트하는것) mocking을 사용하기도 한다.

테스트는 실행 순서를 보장할 수 없기 때문에 각각 독립적이어야 한다.

DTO의 존재이유

DTO의 존재이유 중 하나는 웹에 편하게 보여주기 위한 것이다.

상태 != 필드

필드를 가지고 있다고 상태를 가지고 있지는 않는다. 예를 들어 service 클래스의 경우 controller가 service를 필드로 가지고 있어도 상태를 가진것이 아니다. 하지만 controller가 domain을 필드로 가지고 있다면 상태를 가진것이다. controller가 상태를 가지게 되면 여러 유저나 스레드가 상태를 공유하게 되는 경우 문제가 발생할 수 있다.

SQL Exception

대부분의 SQLException은 복구가 불가능하다. 복구가 불가능한 예외는 throws로 예외를 던지기 보다는 unchecked나 RuntimeException으로 전환해줘야 한다. 이때 커스텀 예외를 만들어서 던져줘도 된다. 또한 e.printStackTrace() 보다는 별도의 에러페이지를 만들어주는 등의 방법으로 웹에서 예외를 확인할 수 있게 해줘야한다.

참고

DAO vs Repository pattern

DAO: 대부분 DB 테이블과 일치.
Repository: DAO와 유사하지만, 비즈니스 로직에 더 가까운 높은 수준. DAO를 사용하여 DB에서 데이터를 가져오고 도메인 객체를 채운다.

DAO는 데이터에 직접 접근. repository는 비즈니스 로직을 구현
(이해되면 추후에 보충하기)

참고
DAO vs Repository Patterns

설정을 위한 정보는 분리

보통은 별도의 파일로 분리한다.
1. 변경의 주기가 코드와 다르다.
2. 환경(개발 환경/실제 환경)에 따라 다른 값을 넣어줘야 할 때가 많다.
3. 위치를 빠르게 찾아 수정할 수 있어야 한다.

HTTP 상태코드

  • 1xx: 조건부 응답
  • 2xx: 성공
  • 3xx: 리다이렉션
  • 4xx: 클라이언트 문제
  • 5xx: 서버 문제

참고

JVM Runtime Data Area

Runtime Data Area는 JVM이 프로그램을 수행하기 위해 OS로부터 할당받는 메모리를 말한다.

PC Register | JVM stack | native method stack | heap | method area

위와 같이 구성되어 있으며 PC Register, JVM stack, native method stack은 스레드 별로 별도로 가지고 있으며 heap, method area는 모든 스레드가 공유한다.

이중 heap에 인스턴스 같은 것들이 저장되는데, 모든 스레드가 공유하기 때문에 동기화 문제가 발생할 수 있다.

참고

읽은 도서

0개의 댓글