티스토리 뷰
강의 후기/TDD, Clean Code with Java 14기
[NextStep] TDD, Clean Code with Java 2강 후기
pansy0319 2022. 4. 23. 20:35반응형
[TDD, Clean Code with Java - https://edu.nextstep.camp/c/8fWRxNWU/]
2022.04.20
- 도메인 지식, 객체 설계 경험이 있는 경우
- 요구사항 분석을 통해 대략적인 설계 - 객체 추출
- UI, DB 등과 의존관계를 가지지 않는 핵심 도메인 영역을 집중 설계
-
- 로직 테스트할 때 어려운 코드가 섞여 있으면 TDD하기 어려움 ex) Random
- 1차적으로는 도메인 로직을 테스트하는 것에 집중!!
- 도메인 객체에 대해서만 TDD를 할 때 쉬움
- 도메인 <-> 단위 테스트
- MVC라 치면 도메인(모델) 영역에 단위테스트를 집중하자
- 이것만 잘해도 상당히 안정적인 애플리케이션을 개발할 수 있다
- 도메인 영역에 객체지향체조 지키는 것에 가장 집중!! (컨트롤러 뷰는 동작하기만 해도...)
- 대략적인 도메인 객체 설계
- RacingCar car 분리하고
- Random을 분리해서 TDD를 할 수 있게...
- 그래도 막막하다면..
- 단위 테스트도 없고, TDD도 아니고, 객체 설계도 하지 않고, 기능 목록을 분리하지도 않고 지금까지 익숙한 방식으로 일단 구현
- 구현하려는 프로그래밍의 도메인 지식을 쌓는다
- 구현할 기능목록을 작성하거나 설계를 잘하려면 도메인 지식이 많아야 기능도 작게 잘 쪼갤 수 있고 객체 설계도 잘 할 수 있음
- 구현한 모든 코드를 버린다
- 아무것도 없는 상태에서 새롭게 구현하는 것보다 레거시 코드가 있는 상태에서 리팩토링하는 것은 몇 배 더 어려움
- 리팩토링이 안되면 걍 싹 다 날린 다음에 처음부터 하는게 더 좋을지도...
- 구현할 기능 목록 작성 또는 간단한 도메인 설계
- 기능 목록 중 가장 만만한 녀석부터 TDD로 구현 시작
- 한번에 모든걸 다 잘하려하면 잘 안되고 재미없다.. 재밌게 꾸준히 하는 것이 더 중요하다!!
- 복잡도가 높아져 리팩토링하기 힘든 상태가 되면 다시 버린다
- 다시 도전
- 질문: 4단계할 때 기능록록에서 기능별로 TDD했는데 익숙해지면 도메인 설계하고 도메인별로 TDD하는게 좋은가요?
- 기능목록을 한 번 만들면 바꾸면 안된다고 생각하는 사람들이 많은데 그건 아니다
- 기능목록은 만들면서 계속 업데이트해나가는것!
- 질문: 도메인 관련 기능이 왔다갔다하는데 어떻게 하는게 좋을까요?
- 리팩토링하답면 계속 새로운 도메인이 생길 수 있다
- 클래스를 왔다갔다하는 상황이 발생하게 되는데 이건 당연하다
- 클래스 분리 리팩토링을 해야하기 때문에..
- 처음에 기능 개발할 때는 한 객체를 대상으로 단위테스트를 만들텐데 리팩토링하게되면 여러 객체가 등장할 수 있다
- 기능목록을 작성한 후 테스트 가능한 부분을 찾아 TDD로 도전한다
- 가능한 작은 단위로 개발 범위/기능을 쪼개는게 좋다
- 작으면 작을수록 좋다
- 아무리 고민해도 TDD 못하겠으면 일단 패스!
- 가능한 부분부터 하자!!
* 단계별로 TDD하기
- 1단계 - Util 성격의 기능이 TDD로 도전하기 좋음
TDD 난이도 낮음. 시간 투자 대비 효과 낮음
- 1자 이상, 5자 이하의 정상적인 이름인지 확인
- 자동차 이동 거리에 따라 "-" 생성하기
- 2단계 - 로직의 복잡도가 낮으면서 단위 테스트 가능한 기능을 TDD로 도전
TDD 난이도 낮음. 시간 투자 대비 효과 낮음- 참여자 이름 split하고 자동차 생성
- 3단계 - 로직의 복잡도가 높으면서 단위 테스트 가능한 기능을 TDD로 도전
TDD 난이도 중간. 시간 투자 대비 효과 높음
로직의 복잡도가 높은 만큼 요구사항 변경, 리팩토링의 빈도가 상대적으로 높음- 경주에 참여한 자동차 중에서 우승자 찾기
- 4단계 - 단위 테스트하기 어려운 부분을 TDD로 도전
TDD 난이도 높음. 시간 투자 대비 효과는 기능에 따라 다름- 자동차 이동 유무
- 우승자 이름 출력하기
- 데이터베이스 CRUD
- 라이브 코딩
public class WinnersTest{
@Test
void findWinners() {
List<Car cars = new ArrayList<>();
cars.add(new Car("pobi"));
cars.add(new Car("json"));
cars.add(new Car("cu"));
List<Car> winners = findWinners(cars); // 테스트할 메소드의 인풋과 메소드를 먼저 정의하고
assertThat(winners); // 잘 실행됐느지 검증하는 코드 넣기
// 누가 우승자일까? -> 지금 다 0ㅇ이니까 셋 다 우승자!!
// 내가 포비를 우승자를 만들고 싶다면 어떻게 해야할까?
// -> 포비만 move해주면 되겠다!!
}
}
//////
public class WinnersTest{
@Test
void findWinners() {
List<Car cars = new ArrayList<>();
Car pobi = new Car("pobi")
cars.add(pobi);
pobi.move(); // -> move가 랜덤인데 얘가 이길꺼라고 어떻게 아냐?
// -> 테스트하기 어려운 코드,, 언제는 성공하고 언제는 실패해서...
// 우리가 원하는건 포비가 가장 많이 이동한 상태를 만드는 것!!
// position 상태가 가장 먼 것을 만드는게 목표.. racing이 끝난 상태!!
// move에 int random을 parameter로 넣으면 우리가 지정함으로 인해서 언제든지 조작가능함
// -> pobi.move(4); json.move(0) => 이렇게 하면 테스트 데이터 준비하는게 지저분해지고 읽기도 힘듦
// 테스트 가능상태를 만들기 위한 것으로 단위테스트 복잡도 증가..
// 생각의 전환 필요! 내가 원하는 포지션을 가질 수 있게 만들면 엄청 쉬울텐데
// -> 거꾸로 생각해서 인위적으로 상태를 만들려니까 좀 힘든데
// 그냥 한 번에 생성자를 통해서 포지션을 넣어버리면? 위치값을 만들 수 있다면 간단해지고 쉬워진다.
cars.add(new Car("json"));
cars.add(new Car("cu"));
List<Car> winners = findWinners(cars);
assertThat(winners);
}
}
///////
public class WinnersTest{
@Test
void findWinners() {
List<Car cars = new ArrayList<>();
cars.add(new Car("pobi", 4);
cars.add(new Car("json", 0));
cars.add(new Car("cu", 0)); // car의 구조를 바꿔서 레이싱이 끝난 차를 만들게 하자! -> 새로운 생성자 만들기
List<Car> winners = findWinners(cars);
assertThat(winners);
}
}
// Car 를 수정하자!
// position을 받는 생성자를 추가!!
class Car {
String name;
int position;
Car(String name) { // 인자가 작은 생성자에서는 초기화X this()를 호출해서 넘기기!!
this(name, 0);
}
Car(String name, int position) {
this.name = name;
this.position = position;
}
}
- 테스트 코드를 위해서 프로덕션 코드를 바꿔도 되나?
- 테스트 코드를 위하든 외부에서 편하게 생성자를 추가하는 것은 언제나 환영한다...
- 생성자를 여러개 만드는건 얼마든지 해도 괜찮다! 거부감이 없어도 된다!
- 그럼 하지 말아야 하는것은?
- 테스트를 위한 메소드를 만드는 건 절대 안된다!!
- 테스트를 위해 메소드를 추가하는 건 안티 패턴이다
- 생성자 추가는 왜 괜찮을까?
- 생성자를 많이 추가하면 추가할수록 이 객체를 만들어서 사용하는 다른 개발자들은 편의성이 높아진다
- 확장성도 좋아진다
- 완전히 상태가 다른 인스턴스를 생성하는 것이라서 포지션을 다른 값으로 생성을 해도 문제가 없다. 영향을 미칠수가 없다
- setter 메소드나 외부에서 값을 변경할 수 있게는 웬만하면 하지 말아라
- 생성자나 final로 해라! 그것이 안전한 코드를 만들 수 있는 좋은 습관이다
- 의식적으로 생성자를 활용하도록 노력해라!!
public class Winners {
public static List<Car> findWinners(List<Car> cars) {
int maxPosition = 0;
for (Car car : cars) {
if (maxPosition < car.getPosition()) { // 부등호 고민되면 우린 테스트 코가 있으니까 그걸 믿고 일단 넣어서 해본다...
maxPosition = car.getPosition()
}
}
// 이런 블랭크를 나중에 리팩토링 포인트로 잡을수도 있다
List<Cars> winners = new ArrayList<>();
for (Car car : cars) {
if (car.getPosition() == maxPosition) {
winners.add(car);
}
}
return winners;
}
}
// equals, hashCode, toString() 굉장히 중요한데 우리가 잘 만드는 버릇이 없음.. 객체 단위로 비교할 때 굉장히 많이 중요함...
// equals 만 구현하면 특정 자료구조에서 문제가 발생할 수 있어서 hashCode랑 쌍으로 같이 구현하는게 좋다(왜 그런지는 찾아보기)
//////
public class Winners {
public static List<Car> findWinners(List<Car> cars) {
return findWinners(cars, maxPosition(cars));
}
public static List<Car> findWinners(List<Car> cars, maxPosition) {
List<Cars> winners = new ArrayList<>();
for (Car car : cars) {
if (car.getPosition() == maxPosition) { // 데이터를 꺼내지 말고 카에게 maxPosition을 보내면서 같냐고 물어보자!!
// 상태를 가진 도메인 객체에게 메시지를 보내자
// 이런 사고를 계속 할 수 있어야 한다!!
winners.add(car);
}
}
return winners;
}
private static int maxPosition(List<Car> cars) {
int maxPosition = 0;
for (Car car : cars) {
if (maxPosition < car.getPosition()) {
maxPosition = car.getPosition()
}
}
return maxPosition;
}
}
// Car 에 isMaxPosition()이라는 함수를 만들어서 데이터를 꺼내지 않고 >메세지를 넘겨서< maxPosition인지 검사하기!!
// +) 한 라인이면 끝나는거 굳이 if else 하면서 하지말아라
public boolean isMaxPosition(int maxPosition) {
return this.position == maxPosition;
} // -> good
public boolean isMaxPosition(int maxPosition) {
if (this.position == maxPosition) {
return true;
} else {
return false;
}
} // -> bad
// 3항 연산자는 가독성이 떨어져서 지양하신다.... ㅋㅋㅋ가독성 사망연산자...
// getter 메서도를 쓰게 됐을 때 자꾸 의식을 전환해서 메시지를 보낼 수 없을까? 생각해보기
// 그럼 getter를 줄일 수 있어서 최소화 할 수 있다
// getter를 최소화할 수 있게 계속 노력해야 된다! 테스트하기가 쉬워지게!!!
public class Winners {
public static List<Car> findWinners(List<Car> cars) {
return findWinners(cars, maxPosition(cars));
}
public static List<Car> findWinners(List<Car> cars, maxPosition) {
List<Cars> winners = new ArrayList<>();
for (Car car : cars) {
if (car.isMaxPosition(maxPosition)) { // 메시지를 보내면 테스트하기가 쉬워지고 중복로직이 줄어든다
// car.getPosition() == maxPosition -> 이런식으로 하면 다른데서 비교할 일 있을 때 계속 적어야 되서 여러곳에 중복으로 생김
// 이걸 처리하는 부분의 요구사항이 달라지면 이 비교하는 부분을 찾아서 다 바꿔 야됨....
// getter 메서드를 사용하지 말라는 것에서 이 부분이 중요함!!
// 외부 개발자느 그냥 isMaxPosition()을 갖다쓰기만 하면되어서
// 조금조금씩 응집도가 높아진다!! 중요한 부분!!
winners.add(car);
} // stream을 쓰지 않는한 indent를 줄이기 힘들다! 그럼 이제 스트림 연습해서 정리해보자 filter라던가 메서드를 써서 구현할 수 있다
// 객체 지향 연습하는데 스트림 쓰는것이 저해가 되서 객체 지향 연습이 익숙해지고 getter 쓰기보다 메세지 쓰는게 익숙해지면 스트림을 써보자
}
return winners;
}
private static int maxPosition(List<Car> cars) {
int maxPosition = 0;
for (Car car : cars) {
if (maxPosition < car.getPosition()) { // getter를 두개나 쓰는 죄악을... getter를 줄이고 인덴트도 줄일 수 있으려나?
maxPosition = car.getPosition()
}
}
return maxPosition;
}
}
// 테스트 먼저 만들고 컴파일 에러 나서 거꾸로 만드는거에 익숙해져라~~
public int maxPosition(int maxPosition) {
if (maxPosition < this.position) {
return this.position;
}
return maxPosition;
}
public class Winners {
public static List<Car> findWinners(List<Car> cars) {
return findWinners(cars, maxPosition(cars));
}
public static List<Car> findWinners(List<Car> cars, maxPosition) {
List<Cars> winners = new ArrayList<>();
for (Car car : cars) {
if (car.isMaxPosition(maxPosition)) {
winners.add(car);
}
}
return winners;
}
private static int maxPosition(List<Car> cars) {
int maxPosition = 0;
for (Car car : cars) {
maxPosition = car.maxPosition(maxPosition) // 메시지를 보내면 로직이 도메인으로 이동하게 됨
// 처음에는 default로 하다가 나중에 필요할 때 public으로 여는 것도 좋다
// 접근제어자의 접근 레벨을 서서히 높이는 것이 훨씬 안전한 코드를 만들수가 있다!!!
}
return maxPosition;
}
}
- setter는 절대 쓰지 말아라!!생성자로 다 해결이 된다(도메인 객체에 한해서)
- 유일하게 쓸 수 있는 것은 DTO!(레이어간 데이터 전달 등에서)
- 도메인 객체에서는 setter를 허용하면 안된다
- getter 메서드도 계속해서 제거할 수 있다. 웬만해선 쓰지 말자!
- view 출력에서는 필요할 수도 있는데 그런 제일 필요한 곳에서만 open 하자!
- 무의식적으로 getter, setter을 만들지 말자! 안 좋은 습관이다
- Lombok은 DTO 같은 곳에서만 쓰고 도메인 객체에서는 쓰지 말자
- @Getter 같은거 습관적으로 붙이지말자... 무조건 편한게 좋은 것은 아니다
- DTO 같은 단순 반복적인 것에는 있는게 편함
- jackson, jpa orm 같은 프레임워크를 사용할 때는 어쩔 수 없다
- 그 속에서 원리원칙주의자라면 프레임워크에 종속되지 않고 도메인을 분리 매핑해서... 어떻게 하는 방법이 있을 수 있지만 중복코드가 엄청 많이 생기게 됨
- 실용주의 노선으로 가서 가능한 매핑도 하고 도메인 객체 역할도 하게 하는데 도메인 객체의 복잡도에 따라서 달라진다...
- 아무튼 필요한 것만 오픈하자!!
- 앞으로 getter 메서드가 보이면 메시지를 보낼 방법이 없나 생각해보자!
- 대화하듯이!! 메시지를 보낼 수 있나하고!!
- 여러 방향으로 고민해보자... isMaxPosition 대신 isWinner 이렇게 도메인 관련 메서드로 간다던가..
- 메시지를 보낼 때에는 도메인 객체랑 대화한다고 생각해서 일을 시키자!!
오늘의 핵심!! - 생성자를 만들어라! 생각의 전환! - 테스트를 위해 만드는건 얼마든지 만들어라! - 메시지를 보내라! 이것만 잘해도 유지보수 하기 쉽고 테스트하기 쉬워질 것이다 |
테스트하기 어려운 부분과 쉬운 부분을 설계를 개선해서 분리할 수 있어야 한다!
- 출력, db crud등은 테스트하기 어렵다
- 외부 시스템은 계속 변할 수 있어서 반복할 수 있는 테스트를 만들기가 힘들다
- 우리가 상태를 건들일 수 없는 것들은 테스트하기가 어렵다
단위테스트하기 어려운 코드를 테스트하려면?
- 아예 테스트하지 않기
- 포비의 경우 데이터베이스 crud 같은건 테스트하지 않음(테스트까지의 비용이 너무 크다)
- ui, 컨트롤러도 단위 테스트하지 않는다
- 그럼 이 테스트하지 않은 부분을 어떻게 커버하느냐 -> end to end 테스트를 한다
- 도메인에 대한 단위테스트를 tdd로 하고 인수 테스트(end to end)를 한다(인수 테스트 주도 개발?)
- 브라우저 역할하는 테스트 코드를 만들고 데이터 흘러가는 것을 테스트한다
- 서비스 레이어는 테스트하지 않는다
- 서비스 레이어에 로직 많은데 안 만들어도 되나요? -> 된다
- 정말 많은 사람들이 비즈니스 로직을 서비스 레이어에 넣는데 그렇게 하면 안 된다
- 서비스 레이어에 다 때려넣는 것이 학습 비용이 적고 빨리 개발할 수 있어서 그렇게 하는 사람이 많다ㅠ(si의 악습..?)
- 자동차 이동 유무는 테스트하기 어려운데 필요한 부분이다
- 테스트하기 어려운 부분과 테스트 가능한 부분을 분리할 수 있어야 한다
테스트하기 어려운 부분을 찾아 테스트 가능한 구조로 개선하기
- Object Graph에서 다른 Object와의 의존관계를 가지지 않는 마지막 노드(node)를 먼저 찾는다
- 예를 들어 RacingMain -> RacingGame -> Car와 같이 의존 관계를 가진다면 Car가 테스트 가능한지 확인한다
- 테스트 가능한지 확인하는 것이 중요하다
- Car에 우리가 만들지 않은 Random과 의존 관계를 갖는 것을 발견함
- Random은 우리가 어떻게 할 수 있는 것이 아니다 -> 테스트하기가 힘들다
- Car.move()도 테스트하기 힘들고, RacingGame.race()도 테스트하기 힘들고, RacingMain.main()도 테스트하기 힘들다
- 테스트 가능한 부분까지는 해봐야 한다
- 질문: 우리에게는 mockito라던가 있지 않나요?
- mockito 같은 프레임워크는 TDD, 단위테스트를 제대로 하지 못하는 사람들이 쓴다
- TDD나 단위테스트를 잘하는 사람은 저런 프레임워크를 잘 사용하지 않는다.
- 이런 외부 프레임워크를 하나씩 추가하면 테스트하기 싫고 도배하게 되고 구현비용도 들고 유지보수하기 어려워진다
Object graph에서 테스트하기 힘든 것을 가장 상위로 옮긴다!
- RacingMain -> RacingGame -> Car에서 RacingGame이 Random과 의존관계를 갖게하면 Car는 테스트가 가능하다
- Random을 RacingMain에 옮기면 RacingGame, Car 둘 다 테스트가 가능해진다
- 마지막에 테스트하기 어려운 부분은 안 한다....
- 하지만 우리의 정말 중요한 비즈니스 로직인 도메인 객체에 대한 테스트는 할 수가 있다
// RacingGame
private int getRandom() {
Random random = new Random();
return random.nextInt(MAX_BOUND);
}
private void move() {
}
// Car
// 메소드 시그니처를 바꾸면 의존 관계 있는 곳이 다 컴파일 에러가 난다
// 불안감을 없애면서 안정적으로 단계적으로 리팩토링하는게 좋다... 스텝바이스텝으로 천천히...
// 점진적인 리팩토링과 안정적인 리팩토링...
// 경계값으로 테스트하는게 좋다!!
// equal 기호 들어가느냐 마느냐로 많이 고민하는데 그러니까 중요하다...
// 과도기 단계에서는 중복코드 있어도 괜찮다...
// 컴파일 에러가 적게 나면서 불안감도 덜 생기고 안정감 있게 가능...
// 아무리 복잡하고 두렵더라도 서서히 점진적으로 개발할 수 있다
// 리팩터링이 한번에 끝나지 않더라도 과거에 코드와 미래의 코드가 공존한
// 언제든지 배포 가능한 상태로 만드는게 지속적으로 리팩토링할 수 있는 상태가 되는거다!!
// 리팩토링 주기를 짧게... 할 수 있는 만큼만 하는게 끊임없이 리팩토링할수있는 그걸 만드는..
// 특히 레거시 코드 리팩토링은 일상생활 속에 걍 계속해야되는것.. 물흐르듯이.. 거부감 없이...
- 테스트를 추가해야 하는 시점?
- 켄트백은 100프로 TDD를 하는 것이 아니라 테스트 코드 없이 배포되면 불안한 부분에 테스트를 추가해야 한다는 이야기를 한다
// 원시 값이 뭐냐?
// - Car의 position 같은 값. 이런걸 포장하면 자연스럽게 클래스가 분리가 된다
// PositionTest
@Test
void create() {
Position position = new Position(1);
assertThat(position.getPosition()).isEqualTo(1);
}
// Position
class Position {
private final int position = 1;
Position(int n) {
this.position = n;
}
int getPosition() {
return this.position;
}
}
// -> getter 쓰면 안되는데~~ isSamePosition 같은걸로?
// 여기서 한 단계 더? equals 오버라이딩?
// 꺼내서 비교하지 말고 객체 단위로 비교할 수 있도록 자꾸 해보자!!
// PositionTest
@Test
void create() {
Position position = new Position(1);
assertThat(position).isEqualTo(new Position(1));
}
// equals랑 hashCode를 구현하자!
// Position
class Position {
private final int position = 1;
Position(int n) {
this.position = n;
}
@Override
public boolean equals(Object o) /// 구현
@Override
hashcode // 구현...
}
// 생성자를 다양하게 만드는게 이 position을 만드는 개발자 입장에서 좋다
// 만약에 문자를 쓸 수 있게도 제공한다?
// 생성자는 다양하게 하는게 좋다...
// 관례상 마지막에 존재하는게 인스턴스 초기화하는 생성자이다
// Position
Postion(String position) { // 이게 없다? 그러면 계속해서 new Position(Integer.parseInt(position)) 해야되서 불편하다
this(Integer.parseInt(position));
}
Position(int position) {
this(position);
}
- Position은 작은 클래스
- int의 범위는 상당히 큰 범위인데 레이싱카에서 Position은 항상 0보다 커야함
- 객체가 생성될 때 객체가 유효한 것인지에 대한 책임은 객체가 온전히 져야 함
- -> Position이 생성된다면 Position이 가지고 있는 값은 0이상이라는 것을 보장해야 하는데 그 보장은 Position 객체가 져야한다
// positontest
@Test
void invlalid() {
assertThat(() -> {
new Positoin(-1);
}).isInstanceOf(IllegalArgumentException.class);
}
// Position
Position(int position) {
this(position);
if (position < 0) {
throw IllegalArgumentException("이동거리는 음수가 될 수 없습니다.");
}
}
// -> Positon 객체가 생성됐다는 것은 0이상이라는게 보장이 된 것! 안전하게 쓸 수 있다!!
// Car에 public Car(String name, Position position)도 추가 가능
// 이제 Car에서 position을 primitive 타입이 아니라 객체로 가져간다
// 이렇게 바꾸다보면 positon을 변경하고 증가시키는 부분에 문제가 발생!!
// 저 비즈니스 로직들이 다 Positoin 쪽으로 이동할 수 있다는 의미...
// 객체를 매핑하고 그러다보면 아무일도 안 하는데 위임하는 메소드가 많아지는데 당연하다...
// car.isMaxPosition 대신에 car.getPosition하면 디미터 법칙 위반?
// 로직 이동하면 테스트 코드도 이동한다
// 단일 책임 원칙!! 응집도!!!
// 이것이 원시값 포장이다
// 문자열 포장도 할 수 있다
// Car의 name 포장하면 이름 관련된 로직이 다 포장...
일급 컬렉션?
- cars와 같은 리스트 콜렉션을 포장하는 것이 일급 콜렉션
- class Cars에는 List<Car> cars 변수만 있어야 한다
- 상태와 관련된 로직들은 따라간다..
- 객체 크기가 작아지고 TDD하는 것이 엄청 수월해진다
- TDD를 하기 어려운 것은 클래스가 크고 복잡도가 높아져서이다
반응형
'강의 후기 > TDD, Clean Code with Java 14기' 카테고리의 다른 글
[NextStep] TDD, Clean Code with Java 1강 후기 (0) | 2022.04.07 |
---|
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- docker
- springboot
- gradle
- 스프링
- 자바스크립트
- docker for mac
- clean code
- Spring
- JavaScript
- k8s
- ddd
- docker pull limit
- linuxkit
- 자바
- 코틀린
- 클린코드
- java
- 도커
- ImagePullBackOff
- gasmask
- kotlin In Action
- 쿠버네티스
- back merge
- cacheable
- kotlin
- QuickTimePlayer
- Kubernetes
- IntelliJ
- 도메인주도설계
- 스프링부트
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함