주요글: 도커 시작하기
반응형

요즘 간단한 DB PL SQL 백업 도구를 팀의 막내와 같이 만들고 있다. 오라클 DB의 프로시저 소스를 관리해주는 상용도구가 있겠지만, 비용상의 문제로 그 도구를 사용하지 않기로 결정했다. (음.. 내가 결정한게 아니고 이 기능을 필요로 하는 곳에서 큰 돈을 쓰지 않기로 결정했다.) 그래서, 부득이 오라클 DB의 코드-패키지, 프로시저, 함수-를 주기적으로 백업하는 간단한 어플리케이션을 개발하기로 했다. (음.. 정확히는 타팀의 개발 요청을 받았다.)


요구사항


요구사항은 간단하다.

  • 나: A님, 우리가 만들어야 하는 게 뭐에요?
  • A: DB에 있는 프로시저 소스를 백업해주는 프로그램이요.
  • 나: 백업은 어디다 해요?
  • A: SVN 리포지토리요.
  • 나: 버전 관리를 해야 하는 이유라도?
  • A: 그게 DB 프로시저가 전혀 관리가 안 되기 때문에, 주기적으로 백업해서 변경 이력을 남기고 싶어요.
  • 나: 아, 그럼 매번 전체 프로시저를 백업할 필요는 없겠네요.
  • A: 네, 변경된 것만 커밋하는 식으로 하면 좋겠어요. 변경된 프로시저를 구하는 SQL은 여기(저쪽 팀)에서 제공해줄께요.
오호라, SQL은 제공해 준단다. 우리가 해야 할 건 제공받은 SQL을 이용해서 변경된 PL SQL 코드를 가져오고, 그 코드를 리포지토리에 보관만 하면 된다.
  • 나: 그럼, 지금 바로 시작할 수 있겠는데요. 그런데, 그 쿼리는 언제 주세요?
  • A: 글쎄요. 그거 할 사람이 지금 다른 거 하고 있는데 그거 끝나고 줄께요.
  • 나: (엄..... ) 언제 끝나는데요?
  • A: 아마도 다음주?
  • 나: (엄.................) 네. 일단 할 수 있는 것 먼저 시작하고 있을께요.
구현의 시작

이런, 쿼리를 나중에 준단다. 그렇다고 멍 때리고 있을 이유는 없다. 만들 코드가 많기 때문이다. 변경된 프로시저를 읽어오는 부분은 여기서의 핵심 로직이 아니다. 읽어오는 기능이 있다 치고 핵심 로직과 그 외에 나머지 부분을 모두 만들어 낼 수 있다. (이에 대한 내용은 "저수준의 상세한 내용 없이 구현하기:http://javacan.tistory.com/297" 글을 읽어보기 바란다.)

우리(나+어린개발자1)는 바로 코드를 만들기 시작했다. 가장 먼저 작성한 것은 아래와 비슷한 테스트 코드이다.

public class BackupToolTest {
    @Test
    public void canCreate() {
        BackupTool tool = new BackupTool();
    }

    public class BackupTool {
    }
}

테스트 코드를 먼저 작성한 뒤에, 한 작업은 BackupTool의 행위를 테스트하는 메서드를 작성하는 것이었다.
  • 나: 자 tool의 backup() 메서드를 만들어보자고, 일단 테스트 코드에서 tool을 호출하도록 만들고,
  • 어린개발자1: (열심히 들음)
  • 나: tool 안에서 변경된 코드 목록을 불러와 그걸 로컬 디렉토리에 보관하고, 이를 커밋하도록 만들어야 할 것 같아. tool은 흐름만 관리하게 하고 나머지는 별도 객체가 처리하도록 하자.
  • 어린개발자1: (열심히 들음)
  • 나: 별도 객체가 처리하도록 할 거니까, backup() 메서드를 실행한 뒤에, 그 객체들이 호출되었는지 확인해보면 될 것 같아.
// 빨간색 코드는 컴파일 에러 발생하는 부분
public class BackupToolTest {
    ...
    @Test
    public void runBackup() {
        BackupTool tool = new BackupTool();
        tool.backup();
        verify(dbCodeFinder).findUpdatedDbCodeAfter(any(Date.class));
        verify(localCodeUpdater).update(dbCodeList);
        verify(svnClient).commit();
    }
}

  • 나: 검증 코드를 만들었으니까, 이제 컴파일 에러를 없애 볼까?
  • 어린개발자1: (끄덕임)
  • 나: (아래와 같은 코드를 입력하면서) DbCodeFinder 인터페이스 만들고, Mock 만들고, 나머지도 동일하게 인터페이스와 Mock 만들어주고, Mockito 이용해서 dbCodeFinder의 findUpdatedDbCodeAfter() 메서드가 호출되면 DbCode 목록을 리턴하게 해 주자. 이것들은 아직 구현이 필요 없으니까, 이따가 구현이 필요할 때 실제로 구현하면 될 것 같아.
// 파란색 코드는 새로 추가된 코드
public class BackupToolTest {
    ...
    @Test
    public void runBackup() {
        DbCodeFinder dbCodeFinder = mock(DbCodeFinder.class);
        LocalCodeUpdater localCodeUpdater = mock(LocalCodeUpdater.class);
        SvnClient svnClient = mock(SvnClient.class);

        List<DbCode> dbCodeList = createDbCodeList();
        when(dbCodeFinder.findUpdatedDbCodeAfter(any(Date.class)).thenReturn(dbCodeList);

        BackupTool tool = new BackupTool();
        tool.backup();

        verify(dbCodeFinder).findUpdatedDbCodeAfter(any(Date.class));
        verify(localCodeUpdater).update(dbCodeList);
        verify(svnClient).commit();
    }

    public class BackupTool {
        public void backup() {
        }
    }
    public interface DbCodeFinder {
        public List<DbCode> findUpdatedDbCodeAfter(Date date);
    }
    public interface LocalCodeUpdater {
        public void update(List<DbCode> dbCodeList);
    }
    public interface SvnClient {
        public void commit();
    }
}
  • 나: DbCode에 뭐가 들어올지 아직 모르니까, 일단 클래스만 만들어두자.
  • 어린개발자1: 네~
public class BackupToolTest {
    ...
    ...
    public class DbCode {
    }
}

이후 작업은 테스트를 통과시키기 위한 코드 작업을 진행했고, 몇 차례의 테스트 실패-테스트 통과-코드 정리 과정을 거쳐 아래의 코드가 완성되었다.

public class BackupToolTest {
    @Test
    public void runBackup() {
        DbCodeFinder dbCodeFinder = mock(DbCodeFinder.class);
        LocalCodeUpdater localCodeUpdater = mock(LocalCodeUpdater.class);
        SvnClient svnClient = mock(SvnClient.class);

        List<DbCode> dbCodeList = createDbCodeList();
        when(dbCodeFinder.findUpdatedDbCodeAfter(any(Date.class)).thenReturn(dbCodeList);

        BackupTool tool = new BackupTool();
        tool.backup();

        verify(dbCodeFinder).findUpdatedDbCodeAfter(any(Date.class));
        verify(localCodeUpdater).update(dbCodeList);
        verify(svnClient).commit();
    }

    public class BackupTool {
        private DbCodeFinder dbCodeFiner;
        private LocalCodeUpdater localCodeUpdater;
        private SvnClient svnClient;

        public void backup() {
            List<DbCode> dbCodeList = 
                    dbCodeFinder.findUpdatedDbCodeAfter(getPreviousUpdatedTime());
            localCodeUpdater.update(dbCodeList);
            svnClient.commit();
        }

        private Date getPreviousUpdatedTime() {
            // 어제 날짜 6시 값 리턴
            return ...;
        }

        public void setDbCodeFinder(DbCodeFinder dbCodeFinder) {
            this.dbCodeFinder = dbCodeFinder;
        }
        public void setLocalCodeUpdater(LocalCodeUpdater localCodeUpdater) {
            this.localCodeUpdater = localCodeUpdater;
        }
        public void setSvnClient(SvnClient svnClient) {
            this.svnClient = svnClient;
        }
    }
    public interface DbCodeFinder {
        public List<DbCode> findUpdatedDbCodeAfter(Date date);
    }
    public interface LocalCodeUpdater {
        public void update(List<DbCode> dbCodeList);
    }
    public interface SvnClient {
        public void commit();
    }
    public class DbCode {
    }
}

뭔가 정상적으로 동작하는 코드가 만들어졌다. 이제 테스트 클래스에 중첩 클래스로 구현한 클래스와 인터페이스들을 모두 별도 파일로 분리해냈다. 별 것 없는 테스트 코드를 만들어낸 것 같지만, 우리는 동작하는 코드로 설계를 진행하였다. 초기에 만들어진 클래스와 인터페이스의 설계는 아래와 같다.



DbCode 데이터 도출

주요 인터페이스가 도출되었다. 이제 할 일은 각 인터페이스를 구현하는 것이다. 가장 먼저 진행한 작업은 디렉토리에 읽어온 DbCode를 로컬에 파일로 기록하는 LocalCodeUpdater의 구현이었다. 이를 위한 테스트 코드를 작성했다. 최초에 대화를 주고 받으면 점진적으로 만들어진 테스트 코드는 아래와 같았다.

public class LocalFileCodeUpdaterTest {
    private LocalFileCodeUpdater codeUpdater;

    @Before
    public void setUp() {
        codeUpdater = new LocalFileCodeUpdater();
        codeUpdater.setRepositoryRoot("target/repo");
    }

    @Test
    public void whenNewDbCode_createFile() {
        deleteIfExists("target/repo/S1/PACKAGE/code1.sql");
        DbCode dbCode = dbCode();
        dbCode.setSchema("S1");
        dbCode.setType(Type.PACKAGE);
        dbCode.setName("code1");
        dbCode.setDdl("test ddl");
        List<DbCode> dbCodeList = new ArrayList<DbCode>();
        dbCodeList.add(dbCode);
        codeUpdater.update(dbCodeList);
        assertFileData("target/repo/S1/PACKAGE/code1.sql", dbCode);
    }

    ... // 파일이 존재할 때 수정되는 지 확인하는 테스트 코드
}

 위 테스트 코드를 만드는 과정에서 DbCode가 가져야 할 데이터가 정리가 되었다. 이후 과정은 테스트를 통과시키는 과정에서 LocalFileCodeUpdater의 구현을 완성해 나갔다. 그리고, SvnClient를 구현하는 단계로 넘어갔다.

[이어지는 이야기는 다음에....]



+ Recent posts