테스트코드

숫자 야구 게임

2023. 3. 29. 23:41
목차
  1. 용어 정리
  2. TDD
  3. TDD를 하는 이유
  4. TDD 사이클
  5. TDD 원칙
  6. TDD로 숫자 야구 게임 구현
  7. 1단계 - Util 성격의 기능이 TDD로 도전하기 좋음
  8. 2단계 - 테스트 가능한 부분에 대해 TDD로 도전

Test Driven Development(테스트 주도 개발, TDD)

 

 

용어 정리

  • Produection Code : 프로그램 구현을 담당하는 부분으로 사용자가 실제로 사용하는 소스 코드
  • Test Code : 프로덕션 코드가 정상적으로 동작하는지를 확인하는 코드

 

TDD

TDD = TFD(Test First Development) + 리팩토링

 

TDD란 프로그래밍 의사결정과 피드백 사이의 간극을 의식하고 이를 제어하는 기술
- 켄트벡
TDD의 아이러니 중 하나는 테스트 기술이 아니라는 점이다. TDD는 분석 기술이며, 설계 기술이기도 하다.
- 켄트벡

TDD 및 단위 테스트를 기반으로 개발하려면 To-Do-List가 잘 정리되어 있어야 한다. To-Do-List를 잘 정리했다는 것은 요구사항 분석을 잘했다는 것이다.

 

 

TDD를 하는 이유

  • 디버깅 시간을 줄여준다.
  • 동작하는 문서 역할을 한다.
  • 변화에 대한 두려움을 줄여준다.

 

TDD 사이클

  1. 실패하는 테스트를 구현한다.
  2. 테스트가 성공하도록 프로덕션 코드를 구현한다.
  3. 프로덕션 코드와 테스트 코드를 리팩토링한다.

이 사이클을 반복한다.

 

 

TDD 원칙

  1. 실패하는 단위 테스트를 작성할 때까지 프로덕션 코드를 작성하지 않는다.
  2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  3. 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

 

 

TDD로 숫자 야구 게임 구현

기능 요구 사항

 

기본적으로 1부터 9까지 서로 다른 수로 이루어진 3자리의 수를 맞추는 게임이다.

  • 같은 수가 같은 자리에 있으면 스트라이크, 다른 자리에 있으면 볼, 같은 수가 전혀 없으면 포볼 또는 낫싱이란 힌트를 얻고, 그 힌트를 이용해서 먼저 상대방(컴퓨터)의 수를 맞추면 승리한다.
    • e.g. 상대방(컴퓨터)의 수가 425일 때, 123을 제시한 경우 : 1스트라이크, 456을 제시한 경우 : 1볼 1스트라이크, 789를 제시한 경우 : 낫싱
  • 위 숫자 야구 게임에서 상대방의 역할을 컴퓨터가 한다. 컴퓨터는 1에서 9까지 서로 다른 임의의 수 3개를 선택한다. 게 임 플레이어는 컴퓨터가 생각하고 있는 3개의 숫자를 입력하고, 컴퓨터는 입력한 숫자에 대한 결과를 출력한다.
  • 이 같은 과정을 반복해 컴퓨터가 선택한 3개의 숫자를 모두 맞히면 게임이 종료된다.
  • 게임을 종료한 후 게임을 다시 시작하거나 완전히 종료할 수 있다.

 

실행 결과

숫자를 입력해 주세요 : 123
1볼 1스트라이크
숫자를 입력해 주세요 : 145
1볼
숫자를 입력해 주세요 : 671
2볼
숫자를 입력해 주세요 : 216
1스트라이크
숫자를 입력해 주세요 : 713
3스트라이크 3개의 숫자를 모두 맞히셨습니다! 게임 종료
게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.
1
숫자를 입력해 주세요 : 123
1볼 1스트라이크 …

 

도메인 지식, 객체 설계 경험이 있는 경우

  • 요구사항 분석을 통해 대략적인 설계 - 객체 추출
  • UI, DB 등과 의존관계를 가지지 않는 핵심 도메인 영역을 집중 설계

테스트가 힘든 부분을 분리해야 한다.

 

 

도메인 지식, 객체 설계 경험이 없는 경우

  • 구현할 기능 목록을 작성한 후에 TDD로 도전
  • 기능 목록을 작성하는 것도 역량이 필요한 것 아닌가?
  • 역량도 중요하지만 연습이 필요하다.

 

그래도 막막하다면

  • 단위 테스트도 없고, TDD도 아니고, 객체 설계도 하지 않고, 기능 목록을 분리하지도 않고 지금까지 익숙한 방식으로 일단 구현
  • 구현하려는 프로그래밍의 도메인 지식을 쌓는다.
  • 구현한 모든 코드를 버린다.
  • 구현할 기능 목록 작성 또는 간단한 도메인 설계
  • 기능 목록 중 가장 만만한 녀석부터 TDD로 구현 시작
  • 복잡도가 높아져 리팩토링하기 힘든 상태가 되면 다시 버린다.
  • 다시 도전

 

아무것도 없는 상태에서 새롭게 구현하는 것보다 레거시 코드가 있는 상태에서 리팩토링하는 것은 몇 배 더 어렵다.

 

일단 구현

  • 지금까지 자신에게 익숙한 방법으로 일단 구현
  • 모든 코드를 버리고 TDD로 다시 구현

 

기능 목록을 작성한 후 테스트 가능한 부분을 찾아 TDD로 도전

  • 1~9의 숫자 중 랜덤으로 3개의 숫자를 구한다.
  • 사용자로부터 입력받는 3개 숫자 예외 처리
    • 1~9의 숫자인가?
    • 중복 값이 있는가?
    • 3자리인가?
  • 위치와 숫자 값이 같은 경우 - 스트라이크
  • 위치는 다른데 숫자 값이 같은 경우 - 볼
  • 숫자 값이 다른 경우 - 낫싱
  • 사용자가 입력한 값에 대한 실행 결과를 구한다.

기능 목록은 계속해서 바뀌어 나가는 것이다. 작업을 하다 보면 예상하지 못했던 기능이 필요할 수 있다. 이 경우 기능 목록이 업데이트됨. 완료한 기능과 완료하지 않은 기능을 분리하면서 기능 구현을 하다 보면 완성!

 

 

1단계 - Util 성격의 기능이 TDD로 도전하기 좋음

  • 사용자로부터 입력받는 3개 숫자 예외 처리
    • 1~9의 숫자인가?
    • 중복 값이 있는가?
    • 3자리 인가?

예시 ) 1~9의 숫자인가?

package baseball;

import org.junit.jupiter.api.Test;

public class ValidationUtilsTest {

    @Test
    void 야구_숫자_1_9_검증() {
        boolean result = validNo(9);
        asserThat(result).isTrue();
    }
}

 

input과 output을 먼저 정하는 것이 중요하다. 클래스는 그다음이다.

 

 

Test 클래스에서 클래스를 바로 구현해도 되는데, 클래스를 바로 사용하고 싶다면

main에도 동일한 패키지가 있어야 나온다.

이렇게 거꾸로 만들어 가는 게 익숙해져야 한다. 이러면 컴파일 에러가 해결됨. 그리고 실행

1단계 : 실패하는 테스트

 

package baseball;

public class ValidationUtils {
    public static boolean validNo(int i) {
        if (i <= 9) {
            return true;
        }
        return false;
    }
}

2단계 : 성공하는 테스트

 

3단계 : 리팩토링 (Ctrl+Alt+Shift+T)

package baseball;

public class ValidationUtils {
    public static boolean validNo(int no) {
        if (no <= 9) {
            return true;
        }
        return false;
    }
}

매개변수명 마음에 안 든다! 변경

 

package baseball;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class ValidationUtilsTest {

    @Test
    void 야구_숫자_1_9_검증() {
        assertThat(ValidationUtils.validNo(9)).isTrue();
    }
}

굳이 로컬변수 필요한가? 삭제하고 직접 넣기

 

한 사이클 종료

 

 

@Test
void 야구_숫자_1_9_검증() {
    assertThat(ValidationUtils.validNo(9)).isTrue();
    assertThat(ValidationUtils.validNo(1)).isTrue();
    assertThat(ValidationUtils.validNo(0)).isFalse();
}

1~9까지의 값이 유효한 지를 체크하는 것이기 때문에 경곗값을 가지고 테스트를 해나가는 것이 중요하다.

실패하면 이때 프로덕션 코드를 수정

package baseball;

public class ValidationUtils {
    public static boolean validNo(int no) {
        if (no >= 1 && no <= 9) {
            return true;
        }
        return false;
    }
}

 

@Test
void 야구_숫자_1_9_검증() {
    assertThat(ValidationUtils.validNo(9)).isTrue();
    assertThat(ValidationUtils.validNo(1)).isTrue();
    assertThat(ValidationUtils.validNo(0)).isFalse();
    assertThat(ValidationUtils.validNo(10)).isFalse();
}

테스트 코드도 나중에 유지보수 해야 하는 코드이기 때문에 , 가능한 적은 테스트 데이터를 가지고 모든 경우의 수를 검증할 수 있는 코드가 좋은 테스트 코드이다.

 

public static boolean validNo(int no) {
    return no >= 1 && no <= 9;
}

 

리팩토링

package baseball;

public class ValidationUtils {

    public static final int MIN_NO = 1;
    public static final int MAX_NO = 9;

    public static boolean validNo(int no) {
        return no >= MIN_NO && no <= MAX_NO;
    }
}

다시 리팩토링. 테스트 코드가 있으니 이렇게도 해보고 저렇게도 해보면서 리팩토링한다. (상수 삽입은 Ctrl+Alt+C)

 

리팩토링까지 다 완성해서 만족하는 수준까지 만들고 전체를 한 번에 커밋하는 것을 추천. 그러면 커밋하기도 명확함.

 

TDD를 연습하기 좋은 것이 알고리즘이다. 알고리즘은 input과 output이 명확함

 

 

2단계 - 테스트 가능한 부분에 대해 TDD로 도전

  • 위치와 숫자 값이 같은 경우 - 스트라이크
  • 위치는 다른데 숫자 값이 같은 경우 - 볼
  • 숫자 값이 다른 경우 - 낫싱

랜덤, DB, UI, 날짜 데이터가 포함되어 있으면 테스트하기 힘들다. 이런 것들이 없는 것에 대해 도전

위의 3가지 경우 테스트 할 때 랜덤값과 별개로 해서 테스트를 수행

 

컴퓨터가 입력한 값 : 123

사용자 값 : 456

이렇게 가정하고 진행. 결괏값은 nothing이 나와야 함

 

123 / 245 -> 1볼

123 / 145 -> 1스트라이크

이처럼 input과 output을 명확하게 가정하고 진행한다.

 

기능이 크면 TDD가 힘들다. 따라서 작게 쪼개는 것이 좋다. 123, 245 이렇게 세 개를 비교하는 것보다 컴퓨터와 사용자의 숫자 하나씩을 비교한다.

 

123 / 456

컴퓨터 1 / 사용자 4 비교해 보자. 이 경우 낫싱이라고 생각할 수 있다.

하지만, 이 정보만 가지고는 스트라이크인지 볼인지 낫싱인지 판별할 수 없다. 왜냐하면 위치값을 모르기 때문이다. 우리가 낫싱이라고 생각하는 것은 123 / 456이 보이기 때문이다. 이것은 숫자 하나씩을 비교한 것이 아니고 전체를 비교한 것이다. 따라서 위치값도 알고 있는 것이 된다.

 

1 / 1 도 마찬가지로 스트라이크라고 생각할 수 있지만, 판별할 수 없다. 이 경우는 볼일수도 있다. 위치값이 둘 다 1이면 스트라이크이다. 하지만, 위치값이 1 / 2 이면 볼이다.

 

이렇게 하면 전체를 비교하는 것보다 더 쉽게 TDD를 할 수 있다.

 

package baseball;

import org.junit.jupiter.api.Test;

public class BallTest {

    @Test
    void nothing() {
        Ball com = new Ball(1, 4);
        BallStatus status = com.play(new Ball(2, 5));
        assertThat(status).isEqualTo(BallStatus.NOTHING);
    }
}

빠르게 컴파일에러 해결

 

package baseball;

public class Ball {
    public Ball(int i, int i1) {

    }

    public BallStatus play(Ball ball) {
        return null;
    }
}
package baseball;

public enum BallStatus {
    NOTHING
}

public BallStatus play(Ball ball) {
    return BallStatus.NOTHING;
}

public class Ball {
    public Ball(int position, int ballNo) {

    }

리팩토링

 

더 이상 추가로 작업할 것이 없음. 다음 단계인 볼 구현

@Test
void ball() {
    Ball com = new Ball(1, 4);
    BallStatus status = com.play(new Ball(2, 4));
    assertThat(status).isEqualTo(BallStatus.BALL);
}

이제 로직 구현

package baseball;

public class Ball {
    private final int position;
    private final int ballNo;

    public Ball(int position, int ballNo) {
        this.position = position;
        this.ballNo = ballNo;
    }

    public BallStatus play(Ball ball) {
        if (ballNo == ball.ballNo) {
            return BallStatus.BALL;
        }
        return BallStatus.NOTHING;
    }
}

통과되면 이제 두 테스트 코드 모두 통과하는지 전체를 테스트

 

리팩토링

package baseball;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class BallTest {

    private Ball com;

    @BeforeEach
    void setUp() {
        com = new Ball(1, 4);
    }

    @Test
    void ball() {
        BallStatus status = com.play(new Ball(2, 4));
        assertThat(status).isEqualTo(BallStatus.BALL);
    }

    @Test
    void nothing() {
        BallStatus status = com.play(new Ball(2, 5));
        assertThat(status).isEqualTo(BallStatus.NOTHING);
    }
}

리팩토링

package baseball;

public class Ball {
    private final int position;
    private final int ballNo;

    public Ball(int position, int ballNo) {
        this.position = position;
        this.ballNo = ballNo;
    }

    public BallStatus play(Ball ball) {
        if (ball.matchBallNo(ballNo)) {
            return BallStatus.BALL;
        }
        return BallStatus.NOTHING;
    }

    private boolean matchBallNo(int ballNo) {
        return this.ballNo == ballNo;
    }
}

private에 접근하니 문제가 없다고 생각할 수 있지만, 가능한 객체에 있는 인스턴스 변수에 직접 접근하기보다는 해당하는 객체에 메시지를 보내는 방향의 사고를 하면 좋다.

 

 

스트라이크 구현

@Test
void strike() {
    BallStatus status = com.play(new Ball(1, 4));
    assertThat(status).isEqualTo(BallStatus.STRIKE);
}

public BallStatus play(Ball ball) {
    if (this.equals(ball))
        return BallStatus.STRIKE;

    if (ball.matchBallNo(ballNo)) {
        return BallStatus.BALL;
    }
    return BallStatus.NOTHING;
}

	...

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Ball ball = (Ball) o;
    return position == ball.position && ballNo == ball.ballNo;
}

@Override
public int hashCode() {
    return Objects.hash(position, ballNo);
}

 

 

기능을 구현하려고 했을 때 너무 커서 TDD로 하기 힘들면, 기능 목록을 너무 큰 단위로 쪼갠 것이다. 그러면 조금 더 작은 단위로 쪼갤 수 없을까를 고민해봐야 한다.

 

 

작은 단위로 구현을 했다면, 다음 단계로 약간 난이도를 높일 수 있다.

 

com / user

123 / 1 4 -> nothing

123 / 1 2 - > ball

123 / 1 1 -> strike

 

컴퓨터의 숫자 전체와 user의 숫자 하나를 비교하는 것이다. com 숫자 하나와 user 숫자 하나 씩만을 비교하는 것보다는 난이도가 높지만, com 숫자 전체와 user 숫자 전체를 비교하는 것보다는 난이도가 낮다.

 

package baseball;

import org.junit.jupiter.api.Test;

import java.util.Arrays;

import static org.assertj.core.api.Assertions.assertThat;

public class BallsTest {

    @Test
    void nothing() {
        Balls answers = new Balls(Arrays.asList(1, 2, 3));
        BallStatus status = answers.play(new Ball(1, 4));
        assertThat(status).isEqualTo(BallStatus.NOTHING);
    }
}
package baseball;

import java.util.List;

public class Balls {
    public <T> Balls(List<T> asList) {

    }

    public BallStatus play(Ball ball) {
        return null;
    }
}

public BallStatus play(Ball ball) {
    return BallStatus.NOTHING;
}

package baseball;

import java.util.List;

public class Balls {
    public Balls(List<Integer> answers) {

    }

    public BallStatus play(Ball userBall) {
        return BallStatus.NOTHING;
    }
}

 

@Test
void ball() {
    Balls answers = new Balls(Arrays.asList(1, 2, 3));
    BallStatus status = answers.play(new Ball(1, 2));
    assertThat(status).isEqualTo(BallStatus.BALL);
}

package baseball;

import java.util.ArrayList;
import java.util.List;

public class Balls {
    private final List<Ball> balls;

    public Balls(List<Integer> answers) {
        List<Ball> balls = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            balls.add(new Ball(i + 1, answers.get(i)));
        }
        this.balls = balls;
    }

    public BallStatus play(Ball userBall) {
        return BallStatus.NOTHING;
    }
}

 

하다보면 중간에 리팩토링을 하기도 한다.

생성자 함수 내부의 코드를 메서드 추출한다. (Ctrl+Alt+M)

package baseball;

import java.util.ArrayList;
import java.util.List;

public class Balls {
    private final List<Ball> balls;

    public Balls(List<Integer> answers) {
        List<Ball> balls = mapBalls(answers);
        this.balls = balls;
    }

    private static List<Ball> mapBalls(List<Integer> answers) {
        List<Ball> balls = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            balls.add(new Ball(i + 1, answers.get(i)));
        }
        return balls;
    }

    public BallStatus play(Ball userBall) {
        return BallStatus.NOTHING;
    }
}

 

다시 리팩토링

public Balls(List<Integer> answers) {
    this.balls = mapBalls(answers);
}

 

public BallStatus play(Ball userBall) {
    return balls.stream()
            .map(answer -> answer.play(userBall))
            .filter(status -> status != BallStatus.NOTHING)
            .findFirst()
            .orElse(BallStatus.NOTHING);
}

리팩토링

public BallStatus play(Ball userBall) {
    return balls.stream()
            .map(answer -> answer.play(userBall))
            .filter(status -> status.isNoNothing())
            .findFirst()
            .orElse(BallStatus.NOTHING);
}

Enum도 하나의 클래스이고, 객체이기 때문에 메시지를 보낼 수 있다.

public BallStatus play(Ball userBall) {
    return balls.stream()
            .map(answer -> answer.play(userBall))
            .filter(BallStatus::isNoNothing)
            .findFirst()
            .orElse(BallStatus.NOTHING);
}

 

@Test
void strike() {
    Balls answers = new Balls(Arrays.asList(1, 2, 3));
    BallStatus status = answers.play(new Ball(1, 1));
    assertThat(status).isEqualTo(BallStatus.STRIKE);
}

 

 

다음 단계로 다시 난이도를 높여 이번엔 공 전체를 비교해보자

 

@Test
void play() {
    Balls answers = new Balls(Arrays.asList(1, 2, 3));
    PlayResult result = answers.play(Arrays.asList(4, 5, 6));
    assertThat(result.getStrike()).isEqualTo(0);
    assertThat(result.getBall()).isEqualTo(0);
}

 

public PlayResult play(List<Integer> userBalls) {
    return new PlayResult();
}
package baseball;

public class PlayResult {
    public int getStrike() {
        return 0;
    }

    public int getBall() {
        return 0;
    }
}

컴파일 에러 빠르게 해결

 

 

@Test
void play_1strike_1ball() {
    Balls answers = new Balls(Arrays.asList(1, 2, 3));
    PlayResult result = answers.play(Arrays.asList(1, 4, 2));
    assertThat(result.getStrike()).isEqualTo(1);
    assertThat(result.getBall()).isEqualTo(1);
}

 

 

package baseball;

import java.util.ArrayList;
import java.util.List;

public class Balls {
    private final List<Ball> answers;

    public Balls(List<Integer> answers) {
        this.answers = mapBalls(answers);
    }

    private static List<Ball> mapBalls(List<Integer> answers) {
        List<Ball> balls = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            balls.add(new Ball(i + 1, answers.get(i)));
        }
        return balls;
    }

    public PlayResult play(List<Integer> balls) {
        Balls userBalls = new Balls(balls);
        PlayResult result = new PlayResult();
        for (Ball answer : answers) {
            BallStatus status = userBalls.play(answer);
            result.report(status);
        }
        return result;
    }

    public BallStatus play(Ball userBall) {
        return answers.stream()
                .map(answer -> answer.play(userBall))
                .filter(BallStatus::isNoNothing)
                .findFirst()
                .orElse(BallStatus.NOTHING);
    }
}

 

package baseball;

public class PlayResult {
    public int getStrike() {
        return 0;
    }

    public int getBall() {
        return 0;
    }

    public void report(BallStatus status) {
        if (status.isStrinke()) {
            
        }
    }
}

 

package baseball;

public enum BallStatus {
    BALL, STRIKE, NOTHING;

    public boolean isNoNothing() {
        return this != NOTHING;
    }

    public boolean isStrinke() {
        return this == STRIKE;
    }
}
package baseball;

public class PlayResult {

    private int strike = 0;
    private int ball = 0;

    public int getStrike() {
        return this.strike;
    }

    public int getBall() {
        return this.ball;
    }

    public void report(BallStatus status) {
        if (status.isStrinke()) {
            this.strike += 1;
        }
        if (status.isBall()) {
            this.ball += 1;
        }
    }
}
package baseball;

public enum BallStatus {
    BALL, STRIKE, NOTHING;

    public boolean isNoNothing() {
        return this != NOTHING;
    }

    public boolean isStrinke() {
        return this == STRIKE;
    }

    public boolean isBall() {
        return this == BALL;
    }
}

 

@Test
void play_3strike() {
    Balls answers = new Balls(Arrays.asList(1, 2, 3));
    PlayResult result = answers.play(Arrays.asList(1, 2, 3));
    assertThat(result.getStrike()).isEqualTo(3);
    assertThat(result.getBall()).isEqualTo(0);
}

@Test
void play_3strike() {
    Balls answers = new Balls(Arrays.asList(1, 2, 3));
    PlayResult result = answers.play(Arrays.asList(1, 2, 3));
    assertThat(result.getStrike()).isEqualTo(3);
    assertThat(result.getBall()).isEqualTo(0);
    assertThat(result.isGameEnd()).isTrue();
}
package baseball;

public class PlayResult {

    private int strike = 0;
    private int ball = 0;

    public int getStrike() {
        return this.strike;
    }

    public int getBall() {
        return this.ball;
    }

    public void report(BallStatus status) {
        if (status.isStrinke()) {
            this.strike += 1;
        }
        if (status.isBall()) {
            this.ball += 1;
        }
    }

    public boolean isGameEnd() {
        return strike == 3;
    }
}

'테스트코드' 카테고리의 다른 글

숫자야구게임 - 단위테스트  (0) 2023.03.20
단위테스트  (0) 2023.03.20
  1. 용어 정리
  2. TDD
  3. TDD를 하는 이유
  4. TDD 사이클
  5. TDD 원칙
  6. TDD로 숫자 야구 게임 구현
  7. 1단계 - Util 성격의 기능이 TDD로 도전하기 좋음
  8. 2단계 - 테스트 가능한 부분에 대해 TDD로 도전
'테스트코드' 카테고리의 다른 글
  • 숫자야구게임 - 단위테스트
  • 단위테스트
ewok
ewok
ewok
기록장
ewok
전체
오늘
어제
  • 분류 전체보기
    • 웹개발 교육
      • HTML
      • CSS
      • JavaScript
      • Database
      • Java
      • jQuery
      • Ajax
      • Bootstrap
      • jsp
      • Spring
      • MyBatis
      • 프로젝트
    • JAVA
    • SpringBoot
      • 기초
      • AWS
      • 개인프로젝트
    • Spring Security
    • JPA
    • 테스트코드
    • Error
    • CS
      • 컴퓨터 구조
      • 이산수학
    • 알고리즘
      • 정리
      • Java
    • SQL
    • 자격증
      • SQLD
      • 정보처리기사
    • Git

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • org.springframework.beans.factory.UnsatisfiedDependencyException
  • this
  • 생성자
  • sqld 합격
  • GIT
  • SQLD
  • branch
  • 노랭이
  • merge commit
  • 버전 관리
  • sqld 자격증
  • org.hibernate.tool.schema.spi.CommandAcceptanceException
  • 브랜치
  • base
  • git bash

최근 댓글

최근 글

hELLO · Designed By 정상우.
ewok
숫자 야구 게임
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.