- 소스코드 파일을 컴퓨터에서 실행할 수 있는 독립 소프트웨어 가공물로 변환하는 과정 또는 그에 대한 결과물 이다. - 이를 좀더 쉽게 풀어 말하자면 우리가 작성한 소스코드(java), 프로젝트에서 쓰인 각각의 파일 및 자원 등(.xml, .jpg, .jar, .properties)을 JVM이나 톰캣같은 WAS가 인식할 수 있는 구조로 패키징 하는 과정 및 결과물이라고 할 수 있다.
2. 빌드 도구(Build tool)
- 빌드 도구란 프로젝트 생성, 테스트 빌드, 배포 등의 작업을 위한 전용 프로그램. - 빠른기간동안 계속해서 늘어나는 라이브러리 추가, 프로젝트를 진행하며 라이브러리의 버전 동기화의 어려움을 해소하고자 등장. - 초기의 java 빌드도구로 Ant를 많이 사용하였으나 최근 많은 빌드도구들이 생겨나 Maven이 많이 쓰였고, 현재는 Gradle이 많이 쓰인다. (Ant는 스크립트 작성도 많고, 라이브러리 의존관리가 되지 않아 불편함)
Maven은 무엇인가?
Maven은 자바 프로젝트의 빌드(build)를 자동화 해주는 빌드 툴(build tool)이다. 즉, 자바 소스를 compile하고 package해서 deploy하는 일을 자동화 해주는 것이다.
- Maven은 자바용 프로젝트 관리도구로 Apache Ant의 대안으로 만들어졌다.
- Maven은 Ant와 마찬가지로 프로젝트의 전체적인 라이프 사이클을 관리하는 도구 이며, 많은 편리함과 이점이 있어 널리 사용되고 있다.
Maven의 특징
- Maven은 필요한 라이브러리를 특정 문서(pom.xml)에 정의해 놓으면 내가 사용할 라이브러리 뿐만 아니라 해당 라이브러리가 작동하는데에 필요한 다른 라이브러리들까지 관리하여 네트워크를 통해서 자동으로 다운받아 준다.
- Maven은 중앙 저장소를 통한 자동 의존성 관리를 중앙 저장소(아파치재단에서 운영 관리)는 라이브러리를 공유하는 파일 서버라고 볼 수 있고, 메이븐은 자기 회사만의 중앙 저장소를 구축할수도 있다.
- 간단한 설정을 통한 배포 관리가 가능 하다.
Ant와 Maven의 차이
1. Ant는 비교적 자유도가 높은 편 (Ant : 전처리 / 컴파일 / 패키징 / 테스팅 / 배포 가능)
2. Maven은 정해진 라이프사이클에 의하여 작업 수행하며, 전반적인 프로젝트 관리 기능까지 포함. (Build Tool + Project Management)
Maven LifeCycle
1) LifeCycle - 미리 정해진 빌드순서 - 메이븐은 프레임워크이기 때문에 동작 방식이 정해져있고, 미리 정의하고 있는 빌드 순서가 있다. 이를 라이프사이클(Lifecycle)이라 한다.
◎ Default(Build) : 일반적인 빌드 프로세스를 위한 모델이다. ◎ Clean : 빌드 시 생성되었던 파일들을 삭제하는 단계 ◎ Validate : 프로젝트가 올바른지 확인하고 필요한 모든 정보를 사용할 수 있는지 확인하는 단계 ◎ Compile : 프로젝트의 소스코드를 컴파일 하는 단계 ◎ Test : 유닛(단위) 테스트를 수행 하는 단계(테스트 실패시 빌드 실패로 처리, 스킵 가능) ◎ Pacakge : 실제 컴파일된 소스 코드와 리소스들을 jar, war 등등의 파일 등의 배포를 위한 패키지로 만드는 단계 ◎ Verify : 통합 테스트 결과에 대한 검사를 실행하여 품질 기준을 충족하는지 확인하는 단계 ◎ Install : 패키지를 로컬 저장소에 설치하는 단계 ◎ Site : 프로젝트 문서와 사이트 작성, 생성하는 단계 ◎ Deploy : 만들어진 package를 원격 저장소에 release 하는 단계
LifeCycle 진행순서
- mvn install : 로컬 저장소로 배포
- mvn deploy : 원격 저장소로 배포
- mvn clean : 빌드 과정에서 생긴 target 디렉토리 내용 삭제
- mvn site : target/site에 문서 사이트 생성
- mvn site-deploy : 문서 사이트를 서버로 배포
<build>에서 설정할 수 있는 값을 확인
<finalName> : 빌드 결과물(ex .jar) 이름 설정
<resources> : 리소스(각종 설정 파일)의 위치를 지정할 수 있다.
- <resource> : 없으면 기본으로 "src/main/resources"
<testResources> : 테스트 리소스의 위치를 지정할 수 있다.
- <testResource> : 없으면 기본으로 "src/test/resources"
<Repositories> : 빌드할 때 접근할 저장소의 위치를 지정할 수 있다. 기본적으로 메이븐 중앙 저장소인 http://repo1.maven.org/maven2로 지정되어 있다.
<outputDirectory> : 컴파일한 결과물 위치 값 지정, 기본 "target/classes"
<testOutputDirectory> : 테스트 소스를 컴파일한 결과물 위치 값 지정, 기본 "target/test-classes"
<plugin> : 어떠한 액션 하나를 담당하는 것으로 가장 중요하지만 들어가는 옵션은 제 각각이다.
- <executions> : 플러그인 goal과 관련된 실행에 대한 설정
- <configuration> : 플러그인에서 필요한 설정 값 지정
최종 빌드 순서는 compile => test => package 이다. ① compile : src/main/java 디렉토리 아래의 모든 소스 코드가 컴파일 된다.
② test : src/test/java, src/test/resources 테스트 자원 복사 및 테스트 소스 코드 컴파일 된다.
③ packaging : 컴파일과 테스트가 완료 된 후, jar, war 같은 형태로 압축하는 작업.
2) Phase(단계) Build Lifecycle의 각각의 단계를 Phase라고 한다.
Phase는 의존관계를 가지고 있어 해당 Phase가 수행되려면 이전 단계의 Phase가 모두 수행되어야 한다.
즉, 모든 빌드단계는 이전 단계가 성공적으로 실행되었을 때 실행된다는 것이 Dependency 입니다.
3) Goal - 특정 작업, 최소한의 실행 단위(task).
- 하나의 플러그인에서는 여러 작업을 수행할 수 있도록 지원하며, 플러그인에서 실행할 수 있는 각각의 기능(명령)을 Goal이라고 한다. (각각 Phase에 연계된 Goal을 실행하는 과정을 Build라고 한다.)
- 플러그인의 goal을 실행하는 방법.
■ - mvn groupId:artifactId:version:goal(생략 가능)
■ - mvn plugin:goal
4 Maven 설정파일
1) settings.xml - 메이븐 빌드 툴과 관련한 설정파일 - MAVEN_HOME/conf 디렉토리에 위치 (메이븐 설치 시 기본 제공) - settings.xml의 설정
2) POM(프로젝트 객체 모델(Project Object Model)) - POM은 pom.xml파일을 말하며 pom.xml은 메이븐을 이용하는 프로젝트의 root에 존재하는 xml 파일이다. - Maven의 기능을 이용하기 위해서 POM이 사용된다. - 파일은 프로젝트마다 1개이며, pom.xml만 보면 프로젝트의 모든 설정, 의존성 등을 알 수 있다. - 다른 파일이름으로 지정할 수도 있다.
Maven이 참조하는 설정파일
1) settings.xml
settings.xml은maven tool 자체에 관련된 설정을 담당한다.
MAVEN_HOME/conf/ 아래에 있다. ( * MAVEN_HOME은 환경변수에 설정한 경로)
Maven 자체에 설정 값을 바꾸는 일은 일단 잘 없으므로 넘어가고 기획한대로 pom.xml을 살펴본다.
2) pom.xml
하나의 자바 프로젝트에 빌드 툴로 maven을 설정했다면, 프로젝트 최상위 디렉토리에 "pom.xml"이라는 파일이 생성되었을 것이다.
pom.xml은 POM(Project Object Model)을 설정하는 부분으로프로젝트 내 빌드 옵션을 설정하는 부분이다.
pom.xml은 <project>...</project>로 둘러싸여서 section별로 여러 정보를 나타내며 설정할 수 있다.
▶ 엘리먼트
◎ modelVersion : POM model의 버전
◎ parent : 프로젝트의 계층 정보
◎ groupId : 프로젝트를 생성하는 조직의 고유 아이디를 결정한다. 일반적으로 도메인 이름을 거꾸로 적는다.
◎ artifactId : 프로젝트 빌드시 파일 대표이름 이다. groupId 내에서 유일해야 한다.
◎ version : 프로젝트의 현재 버전, 프로젝트 개발 중일 때는 SNAPSHOT을 접미사로 사용.
◎ packaging : 패키징 유형(jar, war, ear 등)
◎ name : 프로젝트, 프로젝트 이름
◎ description : 프로젝트에 대한 간략한 설명
◎ url : 프로젝트에 대한 참고 Reference 사이트
◎ properties : 버전관리시 용이 하다.
◎ dependencies : dependencies태그 안에는 프로젝트와 의존 관계에 있는 라이브러리들을 관리 한다.
◎ build : 빌드에 사용할 플러그인 목록
1) 프로젝트 정보
제일 위에 태그부터 살펴보도록 한다.
<modelVersion> : 4.0.0이라고 써있는데 이것은 maven의 pom.xml의 모델 버전이다. 형식이 4.0.0 버전이라고 이해하면 된다.
<groupId> : 프로젝트를 생성한 조직 또는 그룹명으로 보통, URL의 역순으로 지정한다.
<artifactId> : 프로젝트에서 생성되는 기본 아티팩트의 고유 이름이다.
메이븐에 의해 생성되는 일반적인 artifact는 <artifact>-<version>.<extention>이다. (ex demo-0.0.1-SNAPSHOT.jar)
<version> : 애플리케이션의 버전. 접미사로 SNAPSHOT이 붙으면 아직 개발단계라는 의미이며, 메이븐에서 라이브러리를 관리하는 방식이 다르다고 한다.
<packaging> : jar, war, ear, pom등 패키지 유형을 나타낸다.
<name> : 프로젝트 명
<description> : 프로젝트 설명
<url> : 프로젝트를 찾을 수 있는 URL
위와 같은 태그들은 프로젝트 정보에 관련된 내용이다.
* <properties> : pom.xml에서 중복해서 사용되는 설정(상수) 값들을 지정해놓는 부분. 다른 위치에서 ${...}로 표기해서 사용할 수 있다. (java.version에 1.8을 적용하고 다른 위치에서 ${java.version}이라고 쓰면 "1.8"이라고 쓴 것과 같다.
* <profiles> : dev, prod 이런식으로 개발할 때, 릴리즈할 때를 나눠야할 필요가 있는 설정 값은 profiles로 설정할 수 있다.
mvn compile -P prod 라고 하면 ${java.version}은 1.9가 된다.
2. 의존성 라이브러리 정보
의존성 라이브러리 정보를 적을 수 있다.
최소한 groupId, artifactId, version 정보가 필요하다.
스프링부트의 spring-boot-starter-*같은 경우에는 부모 pom.xml에서 이미 버전정보가 있어서 version은 따로 지정할 필요가 없다. 특히 스프링부트는 해당 스프링버전에 잘 맞는 버전으로 이미 설정되어 있기 때문에 오버라이드해서 문제가 생기는 부분은 순전히 개발자 탓이다.
그리고 A라는 라이브러리를 사용하는데 B,C,D가 의존성을 가진다면 A를 dependency에 추가하면 자동으로 필요한 B,C,D도 가져오는 기능이 있다.
3. build 정보
build tool : maven의 핵심인 빌드와 관련된 정보를 설정할 수 있는 곳이다.
<build> 부분에서 설정할 수 있는 값들에 대해 설명하기 전에 "라이프 사이클(life-cycle"에 대해서 알 필요가 있다.
객체의 생명주기처럼 maven에는 라이프 사이클이 존재한다.
크게 default, clean, site 라이프 사이클로 나누고 세부적으로 페이즈(phase) 있다
제어의 역전 패턴이 인기를 끄는 이유는 프로그램의 생명주기에 대한 제어권이 웹 애플리케이션 컨테이너에 있기 때문입니다. 즉, 사용자가 직접 new 연산자를 통해 인스턴스를 생성하고 메서드를 호출하는 일련의 생명주기에 대한 작업들을 스프링에 위임할 수 있게 되는 것입니다.
쉽게 말해 개념적으로 IOC(제어의 역전)라는 것은 간단한 의미인데, 원래 개발자가 해왔던 일을 컨테이너라는 객체 관리 프로그램이 알아서 해준다는 것이다. 프레임워크를 쓰는 이유 중의 하나가 내가 할일을 프레임워크가 대신해주니 코딩이 편해지고 빨리지는 것이다
IOC는 직관적이지 못하기 때문에 IOC를 구현하는 방법으로는 의존성 주입(DI)이 있습니다.
2. 의존성 주입(DI)
컨테이너에서 관리할 객체를 지정해주고, 코드(app) 내에서는 컨테이너에서 객체를 받아 사용하는 방식 빈을 정의할 때 객체 간 의존 관계를 명시해 코드에서 사용 시 자동으로 주입 받도록 함 스프링 컨테이너는 어플리케이션 작동 과정에 필요한 여러 객체들을 알아서 생성하고 지우고 관리해준다고 했습니다. DI는 개발자가 컨테이너에서 관리될 객체를 지정해줄 때 의존 객체를 지정해주는 작업을 의미합니다
의존성 주입이란? = (DI : Dependency Injection)
객체 사이에 필요한 의존 관계에 대해서 스프링 컨테이너가 자동으로 연결해 주는 것을 말합니다
Whatever happened next, the framework needed a name. In the book it was referred to as the “Interface21 framework” (at that point it used com.interface21 package names), but that was not a name to inspire a community. Fortunately Yann stepped up with a suggestion: “Spring”. His reasoning was association with nature (having noticed that I'd trekked to Everest Base Camp in 2000); and the fact that Spring represented a fresh start after the “winter” of traditional J2EE. We recognized the simplicity and elegance of this name, and quickly agreed on it.
해석하면 :기존의J2EE의"겨울"로부터 새로운 시작의"봄"이 왔다고 합니다
스프링(String)이란?
스프링 = String -> 자바 기반의 웹 애플리케이션을 만들 수 있는 프레임워크
스프링(Spring)의 정확한 명칭은 스프링 프레임워크(Spring Framework)입니다.
스프링에 대해서 이해하기 위해서는 프레임워크가 무엇인지, 라이브러리와의 차이를 알아야 합니다.
라이브러리는 간단하게 특정 기능을 하는 코드 뭉치이다.
개발자는 자기 코드에 라이브러리를 포함시키고 원하는 기능을 사용해서 개발을 할 수 있다. 더욱 간단하게는 원하는 기능을 하는 함수를 콜해서 사용하는 것으로 라이브러리를 활용한다.
프레임워크는 라이브러리를 포함하는 개념이고, 개발자가 만든 코드를 사용한다. 스프링 프레임워크는 여러 라이브러리를 제공하고, 그것을 활용해서 개발한 프로그램을 동작시킨다.
소스를 어떻게 실행하는지는 코드를 사용하는 프레임워크에 달려있다
스프링 사이트에서는 스프링 프레임워크가 인프라와 관련된 내용을 애플리케이션 레벨에서 설정하도록 해줌으로써 개발자가 코드로 대부분을 컨트롤 할 수 있게끔 지원한다고 설명한다. 즉, 개발자가 코드 안에 애플리케이션 동작에 대한 내용을 기술하면 스프링 프레임워크가 이를 해석해서 동작하는 것이다.
스프링을 사용하는 가장 일반적인 예로는 Servlet API 가 있다. 개발자는 API를 처리할 클래스를 정의하고 이것이 Servlet API 를 위한 클래스임을 표시(어노테이션 활용)한다. 그 이후 프로그램을 실행하면 스프링은 API 요청이 들어오면 해당 클래스를 이용해서 처리한다. 개발자가 Servlet에 관련된 것을 개발하지 않아도 되며, 데이터 바인딩, 객체 생성 등 왠만한 것들은 스프링이 알아서 해준다. 이런 편한 점 때문에 많은 개발자들이 스프링 프레임워크를 사용하고 있다.
스프링 부트란? (Spring Boot) 스프링 프레임워크는 기능이 많은만큼 환경설정이 복잡한 편이다. 이에 어려움을 느끼는 사용자들을 위해 나온 것이 바로 스프링 부트다. 스프링 부트는 스프링 프레임워크를 사용하기 위한 설정의 많은 부분을 자동화하여 사용자가 정말 편하게 스프링을 활용할 수 있도록 돕는다. 스프링 부트 starter 디펜던시만 추가해주면 바로 API를 정의하고, 내장된 탐캣이나 제티로 웹 애플리케이션 서버를 실행할 수 있다. 심지어 스프링 홈페이지의 이니셜라이저를 사용하면 바로 실행 가능한 코드를 만들어준다. 실행환경이나 의존성 관리 등의 인프라 관련 등은 신경쓸 필요 없이 바로 코딩을 시작하면 된다. 그리고 바로 그것이 스프링의 키 포인트이다.
정리
스프링 프레임워크는 라이브러리가 아닌 프레임워크이다. 단순히 코드를 제공하는 것이 아니라 프로그래밍 방법을 제공한다. 이렇게 프로그래밍 방법을 제공하고 개발자가 그에 맞추어 개발하는 방식은 어쩌면 너무 딱딱하고 획일화된 프로그램을 만들 수 있다. 하지만 특정 목적을 위한 프로그램을 만드는 데에 있어서 정형화된 방식은 효율성을 극대화 시켜준다. 그리고 스프링을 처음 사용하거나 웹 애플리케이션 서버를 간단히 만들 때 스프링 부트로 개발을 한다면 정말 빠르고 간편하게 프로그램을 만들 수 있다.
Message Queue(MQ) = 메시지 큐 -> 프로세스 또는 프로그램 간에 데이터를 교환할 때 사용하는 통신 방법 중에 하나로 메시지를 저장하는 공간이다.
Producer = 프로듀서(생산자) -> 생산자로 취급되는 컴포넌트가 메시지를 메시지 큐에 추가한다. 메시지를 생산하고, 메시지를 저장공간인 큐에 전송하는 역할을 한다.
Consumer = 컨슈머(소비자) -> 메시지를 처리하는 방식, 메시지 큐에 저장되어 있는 메시지를 읽어와서 처리하는 방식
Message Broker = 메시지 브로커 ->Publisher(송신자)로부터 전달받은 메시지를Subscriber(수신자)로 전달해주는 중간 역할이며 응용 소프트웨어 간에 메시지를 교환할 수 있게 한다. 메시지의 그룹을Topic(토픽)이라고 한다.
Topic = 토픽 -> 메시지를 저장하는 단위, Producer로 메시지 큐에 메시지를 넣으면 Topic에 저장되고, Consumer로 메시지 큐에서 메시지를 가져갈 때 Topic에서 가져간다. 즉, Producer와 Consumer는 Topic를 기준으로 메시지를 주고받게 된다.
partition = 파티션-> 메세지가 저장되는 물리적인 파일
브로커 = 파티션에 저장되는 메시지를 파일 시스템에 저장, 파일시스템 = "세그먼트 파일"
Publisher / Subscriber = Producer / Consumer
-> 메시지를 메시지 큐의 저장순서는 FIFO방식으로 먼저 들어온 메시지부터 저장되고, 메시지를 가져갈 때는 먼저 들어온 메시지 부터 나가는 방식이다.
메시지 큐를 사용하는 이유
1. 메시지는 consumer로 취급되는 다른 컴토넌트가 메시지를 검색하고, 어떤 작업을 수행할 때까지 작업을 수행한다.
2. 각 메시지는 하나의 소비자에 의해 한번만 처리될 수 있는데, 이러한 이유로 메시지 큐를 이용하는 방식을 일대일 통신이라고 부른다.
일대일통신(유니 캐스팅) / 일대다통신(멀티 캐스팅)
메시지 큐를 이용해서 무엇을 하는가?
- 이메일 전송 = 이메일 발급서비스, 회원가입을 위해 이메일 발급 서비스
- 블로그 포스팅 = 사용자 -> 이미지가 포함된 블로그 포스팅 -> 이미지를 저장소에 전송 -> 이미지 정보가 포함된 메시지를 메시지 큐(저장소)에 저장 -> 메시지 큐(저장소)에서 이미지를 가져와 최적화
메시지 큐의 장점
비동기(Asynchronous): Queue에 넣어두기 때문에 나중에 처리할 수 있다.
분리 또는 비동조(Decoupling): 애플리케이션과 분리할 수 있다.
탄력성(Resilience): 일부가 실패 시 전체에 영향을 받지 않는다.
과잉(Redundancy): 실패 할 경우 재실행이 가능하다.
보증(Guarantees): 작업이 처리된 걸 확인할 수 있다.
확장성(Scalable): 다수의 프로세스들이 큐에 메시지를 보낼 수 있다.
대표적인 메시지 큐(MQ)의 종류 = ActiveMQ(JMS), RabbitMQ, kafka
1. ActiveMQ(JMS)
MOM을 자바에서 지원하는 표준 API이다. JMS는 다른 자바 애플리케이션들끼리 통신이 가능하지만 다른 MOM의 통신은 불가능하다. (AMQP, SMTP 같은)
ActiveMQ의 JMS 라이브러리를 사용한 자바 애플리케이션들끼리 통신이 가능하다. 하지만 다른 자바 애플리케이션(Non ActiveMQ)의 JMS와는 통신할 수 없다.
2. RabbitMQ
RabbitMQ는 AMQP(Advanced Message Queuing Protocol)를 구현한 오픈소스 메시지 브로커이다.
AMQP는 MQ를 오픈 소스에 기반한 표준 프로토콜이다. 프로토콜만 맞다면 다른 AMQP를 사용한 애플리케이션끼리 통신이 가능하다. 플러그인을 통해서 SMTP, STOMP 프로토콜과의 확장이 가능하다.
AMQP는 메세지 전달을 아래 3가지 방식 중 하나를 보장한다.
At-Most-Once: 각 메시지는 한번만 전달되거나 전달되지 않음
At-Least-Once: 각 메시지는 최소 한번 이상 전달됨을 보장
Exactly-Once: 각 메시지는 딱 한번만 전달됨
AMQP는 메시징 제공자와 클라이언트의 동작에 대해 각기 다른 벤더들의 구현체가 상호 운용될 수 있는 정도로까지 권한을 준다. 이는 SMTP, HTTP, FTP 등이 상호 운용이 가능한 시스템을 만든다는 점에서 동일하다.
Producer(Publisher): 메시지를 보내는 곳이다.
Consumer(Subscriber): 메시지를 받는 곳이다.
Exchange: Producer로부터 메시지를 수신하는 곳. 수신한 메시지를 큐에 분배한다.
Queue: 메시지를 저장하는 곳. 저장했다가 Consumer에게 전달한다.
Binding: Exchange와 Queue의 Mapping. 1:1 또는 1:N
Exchange가 Producer로부터 메시지를 받고 Queue에 전달한다. Queue는 Consumer에게 메시지를 전달한다.
3. Apache Kafka
Apache Kafka는 LinkedIn이 개발하고 Apache Software Foundation에 기부한 오픈 소스 스트림 프로세싱 소프트웨어 플랫폼이다.
높은 처리량을 요구하는 실시간 데이터 피드 처리나 대기 시간이 짧은 플랫폼을 제공하는 것을 목표로 하며 TCP 기반 프로토콜을 사용한다. 클러스터를 중심으로 Producer와 Consumer가 데이터를 Push하고 Pull하는 구조를 가진다.
특징
Publisher / Subscriber 모델
고가용성
확장성
디스크 순차 저장 및 처리
분산 처리 (Partitioning)
카프카는 내구성이 뛰어난 메시지 저장소로, 고객들은 메시지가 한 번 배달되면 대기열에서 제거되는 전통적인 메시지 중개업자들과는 달리,필요에 따라 이벤트 스트림을 재생할 수 있다.
대용량의 실시간 로그 처리에 특화되어 설계된 메시징 시스템으로써 기존 범용 메시징 시스템대비TPS(Transaction per second)가 매우 우수하다.단, 특화된 시스템이기 때문에 범용 메시징 시스템에서 제공하는 다양한 기능들은 제공되지 않는다.
분산 시스템을 기본으로 설계되었기 때문에 기존 메시징 시스템에 비해분산 및 복제 구성을 손쉽게 할 수 있다.
AMQP 프로토콜이나 JMS API를 사용하지 않고 단순한 메시지 헤더를 지닌 TCP기반의 프로토콜을 사용하여프로토콜에 의한 오버헤드를 감소시켰다.
Producer가 Broker에게 다수의 메시지를 전송할 때 각 메시지를 개별적으로 전송해야하는 기존 메시징 시스템과는 달리,다수의 메시지를 batch형태로 Broker에게 한 번에 전달할 수 있어 TCP/IP 라운드 트립 횟수를 줄일 수 있다.
메시지를 기본적으로 메모리에 저장하는 기본 메시징 시스템과는 달리메시지를 파일 시스템에 저장한다. (RabbitMQ는 ram or disk 선택가능)
파일 시스템에 메시지를 저장하기 때문에 별도의 설정을 하지 않아도데이터의 영속성이 보장된다.
기존 메시징 시스템에서는 처리되지 않고 남아있는 메시지의 수가 많을수록 시스템의 성능이 크게 감소하였으나,Kafka에서는 메시지를 파일 시스템에 저장하기 때문에 메시지를 많이 쌓아두어도 성능이 크게 감소하지 않는다. 또한 많은 메시지를 쌓아둘 수 있기 때문에, 실시간 처리뿐만 아니라 주기적인 batch 작업에 사용할 데이터를 쌓아두는 용도로도 사용할 수 있다.
Consumer에 의해 처리된 메시지(ack)를 곧바로 삭제하는 기존 메시징 시스템과는 달리 처리된 메시지를 삭제하지 않고 파일 시스템에 그대로 두었다가 설정된 수명이 지나면 삭제한다.처리된 메시지를 일정 기간동안 삭제하지 않기 때문에 메시지 처리 도중에 문제가 발생하였거나 처리 로직이 변경되었을 경우 Consumer가 메시지를 처음부터 다시 처리(rewind)하도록 할 수 있다.
기존의 메시징 시스템에서는 Broker가 Consumer에게 메시지를 Push해주는 방식인데 반해,Kafka는 Consumer가 Broker로부터 직접 메시지를 가지고 가는 pull(poliing)방식으로 동작한다.따라서 Consumer는 자신의 처리능력만큼의 메시지만 Broker로부터 가져오기 때문에 최적의 성능을 낼 수 있다.
메시지를 Pull 방식으로 가져오므로,메시지를 쌓아두었따가 주기적으로 처리하는 Batch Consumer의 구현이 가능하다.
큐의 기능은 JMS, AMQP 기반의 RabbitMQ등에 비해서는 많이 부족하지만 대용량 메시지를 지원할 수 있는 것이 가장 큰 특징이다. 특히 분산 환경에서 복사본을 다른 노드에 저장함으로써 노드 장애에 대한 장애 대응성을 가지고 있는 강점이 있다.
앱 실행 중 메모리에 저장된 데이터는 앱의 실행이 종료되면 모두 사라지기 때문에, 앱의 실행 여부와 관계없이 유지되어야 하는 데이터는, 영구적 보관이 가능한 저장소에 저장을 해두어야 데이터가 삭제되는 것을 방지할 수 있습니다.
이를 위한 방법은 여러 가지가 있습니다. 여기서는 4가지만 언급을 드리겠습니다. 1. SharedPreferences를 사용하여 앱의 설정 정보와 같은 간단한 데이터를 보관하는 방법, 2. 파일 입출력 API를 통해 기기의 저장소(storage)에 데이터를 직접 읽고 쓰는 방법, 3. SQL을 사용하여 로컬 데이터베이스에 데이터를 관리하는 방법, 4. 인터넷을 통해 연결된 서버와 데이터를 주고받는 방법 등, 다양한 방법 위의 4가지 방법으로 데이터를 저장하고 저장된 데이터를 읽어올 수 있습니다.
위의 방법 중 어떤 것을 사용할 것인지는, 앱에서 관리할 데이터의 크기와 형식, 범위, 그리고 각 방법의 장단점을 고려하여 결정할 수 있습니다. 하지만 그 중 가장 범용적이고, 보편적인 방법을 사용해야 한다면, 데이터베이스를 사용하여 데이터를 관리하는 방법을 선택할 수 있습니다.
<데이터베이스 사용 시 장점>
데이터베이스를 사용하면, SharedPreferences의 키-값(key-value) 저장 형식의 한계점 극복, 파일 입출력 방법의 저장 형식 설계에 따른 복잡성 해소, 서버 통신 방법에서 요구되는 서버 구축 비용 절감 등의 장점을 획득할 수 있습니다.
데이터베이스의 동작을 이해하기 위한 개념 학습, 데이터베이스에 저장될 데이터를 위한 스키마 구조 설계에 대한 고민, 데이터베이스에서 값들을 읽고 쓰기 위해 사용되는 SQL에 대한 사용법 숙지 등, 다른 방법들에 비해 비교적 선행적으로 준비해야 할 내용들이 많기는 합니다. 그러나 한번 익혀두면 큰 변화없이 지속적으로 활용할 수 있는 내용들이므로, 데이터베이스를 접하는 초반의 생소함만 극복하면, 프로그램 개발에 쉽게 적용할 수 있습니다.
1.1 데이터베이스 관리 시스템(Database Management System, DBMS)
데이터베이스를 만들고, 데이터를 저장하거나 관리할 수 있도록 만들어주는 소프트웨어를 데이터베이스 관리 시스템(DBMS)이라고 하는데, 현재 제품으로 출시된 데이터베이스 관리 시스템(DBMS, Database Management System)은 그 종류가 매우 다양합니다. Oracle, Mysql, MS-SQL, SQLite 등 개발자라면 누구나 한번 쯤은 이름을 들어본 적 있을 유명한 데이터베이스 시스템이 출시, 판매되고 있죠.
어떤 데이터베이스 관리 시스템(DBMS)을 사용할 것인지는 데이터베이스 관리 시스템에서 제공되는 기능과 데이터 처리 용량, 사용 편의성, 기능 확장성, 시스템 복잡성, 소요 비용 등의 여러 요소를 고려하여 선택할 수 있는데, 다른 프레임워크에 비해 성능과 용량 측면에서 제약이 존재하는 안드로이드에서는 기본적으로 SQLite를 사용합니다.
SQLite(www.sqlite.org)는 서버 단위에서 대용량 데이터를 처리하기 위한 용도보다는, 단일 응용 프로그램에서 비교적 적은 용량의 데이터를 처리하는데 적합한 데이터베이스 관리 시스템(DBMS)입니다. 즉, 안드로이드 앱과 같은 소규모의 프로그램에서 데이터베이스 관리가 필요한 경우 사용하기 적합한 데이터베이스 관리 시스템이죠.
SQLite를 사용하기 위해서 별도의 복잡한 시스템, 서비스, 프로그램이 요구되지 않습니다. 단지SQLite를 위한 API 함수를 호출하는 것만으로 데이터베이스를 위한 기능을 사용할 수 있으며, 데이터베이스 API를 통해 관리되는 모든 데이터는하나의 파일에 저장됩니다. (그래서 SQLite의 데이터베이스를 백업하는 작업은 하나의 데이터베이스 저장 파일을 복사하는 것으로 끝납니다.)
3. 안드로이드에서 SQLite를 사용하기 전 준비 사항.
안드로이드에서 SQLite를 사용하기 위해 기본적으로 알아야 할 내용은 당연히 SQLite를 다루는 클래스에 대한 구조와 API 함수들의 사용법입니다. 하지만 그 전에, 데이터베이스에 대한 기본 개념과 구성 요소 그리고 데이터베이스의 데이터를 관리하는데 사용되는 SQL(Structured Query Language) 등을 알아둘 필요가 있습니다.
데이터베이스에 대한 개념과 이론을 설명한 문서에는 매우 방대한 내용이 포함되어 있습니다. 모든 내용을 제대로 이해하고 습득하려면, 몇 일이 걸릴지, 몇 달이 걸릴지 알 수 없을만큼 많은 내용을 포함하고 있죠. 여기서 그 모든 내용을 설명하려면, 공간도 한참 부족할 뿐더러, 원래 설명하려고 했던 안드로이드에서 데이터베이스를 다루는 내용 따위는 안드로메다로 날아가 버릴 수도 있습니다.
4. 관계형 데이터베이스(Relational Database)
안드로이드에서 기본적으로 제공하는 SQLite는 관계형 데이터베이스 관리 시스템(Relatinoal Database Management System, RDBMS)의 한 종류입니다.
4.1 관계형 데이터베이스에서 데이터 구조화하기.
관계형(Relational) 데이터베이스에서는 데이터베이스 내에 데이터를 저장할 때, 데이터들이 공통적으로 포함하는(그리고 논리적 구분이 가능한) 속성들을 식별한 다음, 각 데이터를 앞서 식별된 속성에 해당하는 값들로 나열하여 저장합니다.
<예시>
과일을 판매하는 과일 가게를 떠올려보죠. 과일 가게에는 아래 그림과 같이, 종류가 다른 과일들이 각각의 바구니에 담겨져 있습니다.
고객의 요구사항은, "가게에 남아 있는 과일의 종류가 어떤 것인지, 종류 별로 몇 개가 남아 있는지를 관리하는 것"입니다. 이 정보를 저장할 때 관계형 데이터베이스를 사용한다면, 데이터베이스에 데이터가 구조화되는 과정은 아래와 같습니다.
4.1.1. 저장하고자 하는 데이터가 공통적으로 가지는 속성 식별하기.
먼저, 데이터가 가지는 공통 속성을 식별해야 합니다. 앞서 한 문장으로 기술된 요구사항에 적혀 있듯이 "1.과일의 종류"와 "2.남은 갯수"를 데이터가 가진 공통적인 속성이 될 수 있죠. 그리고 속성을 식별하고 나면, 속성들을 가로로 나열합니다.
4.1.2 데이터들을 속성에 해당하는 값들로 나열하기.
속성이 식별되고나면, 다음 단계는 각 데이터를 속성에 해당하는 값들로 나열하는 것입니다. 여기서 데이터는 세로 방향으로 나열합니다.
속성의 식별과 속성에 따른 데이터 나열이 완료되면 아래 그림과 같이 테이블 형태의 구조가 완성됩니다.
4.1.3 속성 식별 과정에서의 주의사항.
속성의 식별은 데이터베이스의 데이터 구조화 과정에 있어 가장 중요한, 그리고 데이터베이스 전체 용량을 결정하는 가장 중요한 요소입니다.
<주의사항>
먼저, 속성은 그 값의 크기와 관계없이 데이터들에 공통적으로 포함되는 것이어야 합니다. 만약 위의 과일 가게 관리 예제에서 "다리 갯수"라는 속성을 식별했다고 가정했을 때, 모든 과일은 다리를 가지고 있지 않으므로 "다리 갯수"라는 속성은 전혀 의미가 없는 속성입니다.
다음으로, 속성의 값들은 논리적 구분이 가능해야 합니다. 위의 예제에서, "식물", "동물" 등으로 구분되는 "생물 분류"를 공통 속성으로 식별했다고 가정한다면, 분명 데이터가 가지는 공통 속성임에는 분명하지만, 모든 데이터가 "식물" 값을 가지게 되어 논리적 구분이 불가능합니다. 즉, 모든 데이터가 같은 값을 가지므로 의미없는 속성이 되는 것입니다.
또 한가지, 속성은 데이터베이스를 사용하는 곳에서 기능적으로 유의미해야 합니다. 예제에서, 추가적으로 과일의 "색상" 속성을 추가하면, "색상" 속성은 모든 과일이 공통적으로 포함하고 있고, 각 과일은 저마다의 색을 가지고 있기 때문에, 논리적 구분도 가능합니다. 그런데 새롭게 구분된 "색상"은 요구사항에 적용되지 않는 속성입니다. 즉, 속성을 식별하고 값을 채운다고 해도 전혀 사용되지 않습니다. 과일 종류 별 남은 갯수를 관리하는 게 유일한 목적이기 때문에, "색상"은 무의미한 속성이 되는 것입니다.
4.2 관계형 데이터베이스 용어
관계형 데이터베이스에서 데이터를 속성과 데이터 값으로 구조화하면 2차원 표 형태의 구조가 만들어지는 것을 확인하였습니다. 속성은 가로 방향으로, 데이터 값은 세로 방향으로 나열되었습니다. 데이터들을 속성에 따라 값으로 나열한 2차원 표 형태를, 관계형 데이터베이스에서는 "관계(Relation)"라고 부릅니다. 즉,관계형 테이터베이스에서 데이터를 구조화한다는 것은, 속성과 그 데이터 값들의 "관계(Relation)"를 찾아내고 그것을 표 모양의 구조로 도식화하는 것이라고 볼 수 있는 것입니다.
하지만 "관계(Relation)"라는 용어는, 설명을 아무리 쉽게 해도 그 의미가 쉽게 전달되지 않을 것 입니다. 데이터베이스와 관련하여 범용적으로 사용되지 않은 용어라서 더욱 어려울 것이라 생각이 듭니다. 그래서 일반적으로 "관계(Relation)" 대신 2차원 표 모양에 기인한"테이블(Table)"이라는 용어가 더 익숙하게 사용됩니다.
그리고 관계형 데이터베이스의 "속성(Attribute)" 또한, (행/열에서 '열'을 의미하는)"컬럼(Column)"또는"필드(Field)"라는 용어로 더 자주 사용됩니다. 세로로 나열되는 데이터의 값은 관계형 데이터베이스 이론에서는 "튜플(Tuple)"이라는 용어로 불리지만, 일반적인 경우에 (행/열에서 '행'을 의미하는)"로우(Row)"또는"레코드(Record)"라는 용어로 더 많이 사용됩니다.
여러 용어들이 혼재되어 좀 헷갈릴수도 있습니다. 하지만 관계형 데이터베이스 이론에서 정의하는 용어인 "속성(Attribute)", "튜플(Tuple)" 등은 일반적인 상황에서는 잘 사용되지 않으므로 개념적으로만 알아두시고,"컬럼(Column)" 또는 "필드(Field)", "로우(Row)" 또는 "레코드(Record)" 등의 용어가 더 범용적으로 사용된다는 것을 기억해두시기 바랍니다.
4.3 데이터베이스 스키마(Schema)
앞서 설명한 범용 용어를 사용하여 데이터베이스 구조화를 정리하면, 관계형 데이터베이스에 데이터를 저장하기 위한 가장 기본이 되는 단위는 테이블(Table)이며, 테이블(Table)은 컬럼(Column)과 로우(Row)로 구성되어있습니다.
그래서 개발자가 데이터베이스를 사용하여 데이터를 저장할 때 첫 번째로 해야 할 일은, 관리하고자 하는 데이터의 공통 속성을 식별하여 컬럼(Column)으로 정의하고 테이블(Table)을 만드는 것입니다. 일단, 테이블이 만들어지면 새로운 데이터를 추가할 수 있고, 추가된 하나의 데이터는 하나의 로우(Row)로 삽입됩니다.
그런데 데이터베이스 내에 하나의 테이블만 만들어 사용하는 경우는 거의 없습니다. 물론 요구사항이 매우 단순한 경우라면 테이블 하나에 관리하고자 하는 모든 정보를 저장할 수 있겠지만, 대부분의 경우 하나의 테이블만으로는 고객의 요구사항을 만족시키기 힘들죠. 때론 구조의 복잡도에 따라 수십 개 또는 수백 개의 테이블이 만들어지기도 합니다.
데이터 저장의 가장 기본 단위인 테이블과 (여기서 설명하지 않지만) 인덱스, 뷰, 권한 등과 같은 데이터베이스 요소들은 데이터베이스 내부에서 어떻게 만들어지고 관리되는 것일까요? 음, 구체적인 구현 방법은 정확히 알 수 없지만, 데이터베이스 내부에서 관리되는 요소들은 개념적으로 스키마(Schema)라는 것으로 관리됩니다.
스키마(Schema)라는 단어의 의미를 정의해보자면, "데이터베이스의 테이블 구조 및 형식, 관계 등의 정보를 형식 언어(formal language)로 기술한 것"을 의미합니다. 즉, 스키마는 데이터베이스의 설계도 역할을 수행하며, 테이블, 인덱스, 뷰 등의 구조에 대한 모든 정보를 가진 것을 말하죠.
스키마(Schema)가 데이터베이스의 설계도와 같은 역할을 수행하고, 형식 언어(formal language)로 기술된다는 것은 데이터베이스 관리에 있어서 상당한 이점을 제공해줍니다. 하나의 데이터베이스 스키마(Schema)를 가지면, 다른 시스템에 똑같은 데이터베이스를 만들 수 있다는 것이니까요. 하지만 이를 데이터베이스 백업과 혼동해서는 안되고, 단지 동일한 구조의 데이터베이스를 새로 생성할 수 있다는 의미입니다.
4.4 SQL(Structured Query Language)
SQL(Structured Query Language)은 관계형 데이터베이스 관리 시스템에서 데이터를 관리하기 위해 사용되는 표준 프로그래밍 언어(Language)입니다.데이터베이스 스키마 생성 및 수정, 테이블 관리, 데이터 추가, 수정, 삭제, 조회 등, 데이터베이스와 관련된 거의 모든 작업을 위해 사용되는 언어이므로, 관계형 데이터베이스를 다루기 위해서는 필수적으로 알아야 할 언어입니다.
SQL이 프로그래밍 언어이긴 하지만 Java 또는 C 언어처럼 프로그램을 개발하기 위한 목적이 아닌, 데이터베이스를 관리하기 위해 사용되는 특수 목적 언어입니다.
SQL은 대화식으로 동작합니다. 하나의 기능을 수행하면 실행 결과를 확인한 다음 다른 기능을 실행하는 형태로 동작하는 것입니다. 예를 들어 테이블에 데이터를 추가하고 조회하는 경우, INSERT 명령으로 시작되는 SQL 문장을 실행하고 그 결과가 리턴되면, 그 다음 데이터 조회를 위한 SELECT 명령을 실행할 수 있습니다.
SQL에 정의된 명령어의 종류는 크게 세 가지로 나뉩니다. 이는 명령어가 사용되는 목적 및 대상에 따라 분류되며, 데이터 정의 언어(DDL, Data Definition Language), 데이터 처리 언어(DML, Data Manipulation Language), 데이터 제어 언어(DCL, Data Control Language)가 바로 명령어의 세 가지 종류입니다.
데이터 정의 언어(DDL, Data Definition Language)는 말 그대로 데이터의 구조를 정의하는 것을 말합니다. 테이블(TABLE), 인덱스(INDEX), 트리거(TRIGGER) 등의 개체를 만들고 관리하는데 사용되는 명령들이 데이터 정의 언어에 속하며, CREATE, ALTER, DROP 등이 있습니다.
DML 명령설명
CREATE
테이블(TABLE), 인덱스(INDEX) 등의 데이터베이스 개체 생성.
ALTER
CREATE로 생성된 테이블, 인덱스 등의 데이터베이스 개체 수정.
DROP
CREATE 명령으로 생성된 데이터베이스 개체 삭제.
[SQL-예제] 데이터 정의 언어(DDL) 사용 예.
/* CREATE 명령으로 FRUITSHOP_T 테이블 생성. */
CREATE TABLE FRUITSHOP_T (NAME TEXT NOT NULL, COUNT INTEGER) ;
/* ALTER 명령으로 FRUITSHOP_T 테이블에 새로운 컬럼(COLUMN) 추가. */
ALTER TABLE FRUITSHOP_T ADD SIZE INTEGER ;
/* DROP 명령으로 FRUITSHOP_T 테이블 삭제. */
DROP TABLE FRUITSHOP_T ;
데이터 조작 언어(DML, Data Manipulation Language)는 데이터 정의 언어로 생성된 개체(테이블 등)에 데이터를 추가하거나, 수정, 삭제, 조회 등의 기능을 수행할 때 사용합니다. 주로 사용되는 명령에는 INSERT, UPDATE, DELETE, SELECT 등이 있습니다.
DML 명령설명
INSERT
테이블(Table)에 하나 이상의 데이터 추가.
UPDATE
테이블(Table)에 저장된 하나 이상의 데이터 수정.
DELETE
테이블(Table)의 데이터 삭제.
SELECT
테이블(Table)에 저장된 데이터 조회.
[SQL-예제] 데이터 조작 언어(DML) 사용 예.
/* INSERT 문을 사용하여 FRUITSHOP_T 테이블에 데이터 추가. */
INSERT INTO FRUITSHOP_T (NAME, COUNT) VALUES ('apple', 2) ;
/* UPDATE 문으로 FRUITSHOP_T 테이블 데이터 수정. */
UPDATE FRUITSHOP_T SET COUNT=3 WHERE NAME='apple' ;
/* DELETE 명령으로 FRUITSHOP_T 테이블의 데이터 삭제. */
DELETE FROM FRUITSHOP_T WHERE NAME='apple' ;
/* SELECT 명령으로 FRUITSHOP_T 테이블의 데이터 조회 */
SELECT * FROM FRUITSHOP_T ;
데이터 제어 언어(DCL, Data Control Language)는 데이터를 다루는데 있어 권한을 설정하거나, 데이터의 무결성 처리 등을 수행하는데 사용되는 언어입니다. GRANT, BEGIN, COMMIT, ROLLBACK 등이 이에 속합니다.
DCL 명령설명
GRANT
데이터베이스 개체(테이블, 인덱스 등)에 대한 사용 권한 설정.
BEGIN
트랜잭션(Transaction) 시작.
COMMIT
트랜잭션(Transaction) 내의 실행 결과 적용.
ROLLBACK
트랜잭션(Transaction)의 실행 취소.
[SQL-예제] 데이터 제어 언어(DML) 사용 예.
/* GRANT 명령을 사용하여 FRUITSHOP_T 테이블 권한 설정. */
GRANT ALL ON FRUITSHOP_T TO PUBLIC ;
/* COMMIT 멸영으로 트랜잭션 실행 결과 적용. */
BEGIN TRANSACTION ;
INSERT INTO FRUITSHOP_T (NAME, COUNT) VALUES ('banana', 3) ;
INSERT INTO FRUITSHOP_T (NAME, COUNT) VALUES ('orange', 5) ;
COMMIT TRANSACTION ;
/* ROLLBACK 명령으로 트랜잭션 취소. */
BEGIN TRANSACTION ;
DELETE FROM FRUITSHOP_T ;
ROLLBACK TRANSACTION ;
안드로이드에서 기본적으로 제공하는 데이터베이스는 SQLite입니다. SQLite는 관계형 데이터베이스입니다.
안드로이드에서 SQLite 데이터베이스를 사용할 때 가장 중요한 핵심 요소는 SQLiteDatabase 클래스입니다. SQLite 데이터베이스를 다루는 기능들이SQLiteDatabase클래스에 정의되어 있죠.
SQLite 데이터베이스에 데이터를 저장하고, 읽어들이는 방법에 대해 알아보도록 하겠습니다.
2. SQLiteDatabase 사용 예제
이제 앱 작성을 통해SQLiteDatabase를 사용하여 데이터를 저장하고, 조회하는 방법에 대해 살펴보도록 하겠습니다.
예제로 작성되는 앱의 실행 화면은 아래 그림과 같이 구성됩니다.
데이터베이스는 아래와 같은 구조를 가집니다.
참고로, 보통 데이터베이스를 사용할 때는 동일한 컬럼(Column)에 따라 식별된 많은 양의 로우(Row)를 저장하는데,본문의 예제는 하나의 로우(Row)만 저장합니다.이는 앱의 설정 정보 등을 저장할 때 사용하는 방법인데, 설정 정보는 최종적으로 저장된 단 하나의 값만 있으면 되기 때문입니다.
2.1 MainActivity의 레이아웃 작성.
앱 작성 시, 첫 번째로 할 일은 MainActivity의 레이아웃을 작성하는 것입니다. 앞서 설계한 예제 화면 구성도에 따라, 아래와 같이 MainActivity에 표시될 레아아웃 XML 코드를 작성합니다.
[STEP-1] "activity_main.xml" - MainActivity의 레이아웃 XML 코드 작성.
다음 단계로, SQLite 데이터베이스를 사용하는데 있어 가장 중요한 요소인,SQLiteDatabase클래스의 객체를 초기화하는 코드를 작성합니다.SQLiteDatabase클래스 객체는 MainActivity의 전역에서 사용되므로, 클래스 변수로 선언합니다.
그리고 최초 앱 실행 시, SQLite 데이터베이스 파일이 존재하지 않으면 새로운 파일을 만들고 데이터베이스를 열도록SQLiteDatabase.openOrCreateDatabase()함수를 사용합니다. 이러한 과정은 init_database() 라는 함수에 작성하고, MainActivity의 onCreate() 함수에서 호출하도록 만듭니다.
[STEP-2] "MainActivity.java" - SQLDatabase 클래스 변수 선언 및 초기화.
public class MainActivity extends AppCompatActivity {
SQLiteDatabase sqliteDB ;
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... 코드 계속
sqliteDB = init_database() ;
// 코드 계속 ...
}
// ... 코드 계속
private SQLiteDatabase init_database() {
SQLiteDatabase db = null ;
// File file = getDatabasePath("contact.db") ;
File file = new File(getFilesDir(), "contact.db") ;
System.out.println("PATH : " + file.toString()) ;
try {
db = SQLiteDatabase.openOrCreateDatabase(file, null) ;
} catch (SQLiteException e) {
e.printStackTrace() ;
}
if (db == null) {
System.out.println("DB creation failed. " + file.getAbsolutePath()) ;
}
return db ;
}
// 코드 계속 ...
}
2.3 SQLite 데이터베이스를 새로 만들 때, 테이블 생성. (CREATE TABLE)
SQLite 데이터베이스를 열어SQLiteDatabase에 대한 참조를 확보했다면, 이제 데이터베이스 테이블에 데이터를 추가하거나 조회할 수 있습니다. 그런데 데이터베이스 파일을 새로 생성한 경우라면 테이블은 존재하지 않기 때문에,CREATE TABLE문을 사용하여 테이블을 생성해야 합니다. CREATE TABLE문에IF NOT EXISTS를 더하여 테이블을 생성합니다.
CONTACT_T 테이블 생성을 위한 SQL 문.
CREATE TABLE IF NOT EXISTS CONTACT_T (NO INTEGER NOT NULL, NAME TEXT, PHONE TEXT, OVER20 INTEGER)
아래와 같이 init_table() 이라는 함수를 새로 추가하고, onCreate()에서 init_database() 함수 다음에 호출하도록 작성합니다.
[STEP-3] "MainActivity.java" - SQLDatabase 클래스 변수 선언 및 초기화.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... 코드 계속
sqliteDB = init_database() ;
init_tables() ;
// 코드 계속 ...
}
// ... 코드 계속
private void init_tables() {
if (sqliteDB != null) {
String sqlCreateTbl = "CREATE TABLE IF NOT EXISTS CONTACT_T (" +
"NO " + "INTEGER NOT NULL," +
"NAME " + "TEXT," +
"PHONE " + "TEXT," +
"OVER20 " + "INTEGER" + ")" ;
System.out.println(sqlCreateTbl) ;
sqliteDB.execSQL(sqlCreateTbl) ;
}
}
// 코드 계속 ...
}
2.4 앱 실행 시, 테이블 데이터 조회하여 표시. (SELECT)
최초 앱이 실행되면 데이터베이스에 저장된 데이터를 로딩하여 화면에 표시하는 코드를 작성합니다. 데이터 조회를 위한SELECT문 실행 결과 데이터를Cursor타입으로 리턴받은 다음,moveToNext()함수를 통해 커서로 전달된 레코드 집합(Record Set)을 탐색합니다.
CONTACT_T 테이블에서 모든 데이터를 조회하는 SQL 문.
SELECT * FROM CONTACT_T
데이터를 조회하는 함수는 load_values() 라는 함수에 작성하고, MainActivity의 onCreate() 함수에서 init_table() 함수 바로 다음에 호출합니다.
[STEP-4] "MainActivity.java" - SELECT 문을 통해 데이터 조회 및 표시.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... 코드 계속
sqliteDB = init_database() ;
init_tables() ;
load_values() ;
// 코드 계속 ...
}
// ... 코드 계속
private void load_values() {
if (sqliteDB != null) {
String sqlQueryTbl = "SELECT * FROM CONTACT_T" ;
Cursor cursor = null ;
// 쿼리 실행
cursor = sqliteDB.rawQuery(sqlQueryTbl, null) ;
if (cursor.moveToNext()) { // 레코드가 존재한다면,
// no (INTEGER) 값 가져오기.
int no = cursor.getInt(0) ;
EditText editTextNo = (EditText) findViewById(R.id.editTextNo) ;
editTextNo.setText(Integer.toString(no)) ;
// name (TEXT) 값 가져오기
String name = cursor.getString(1) ;
EditText editTextName = (EditText) findViewById(R.id.editTextName) ;
editTextName.setText(name) ;
// phone (TEXT) 값 가져오기
String phone = cursor.getString(2) ;
EditText editTextPhone = (EditText) findViewById(R.id.editTextPhone) ;
editTextPhone.setText(phone) ;
// over20 (INTEGER) 값 가져오기.
int over20 = cursor.getInt(3) ;
CheckBox checkBoxOver20 = (CheckBox) findViewById(R.id.checkBoxOver20) ;
if (over20 == 0) {
checkBoxOver20.setChecked(false) ;
} else {
checkBoxOver20.setChecked(true) ;
}
}
}
}
// 코드 계속 ...
}
2.5 입력 데이터 저장하기. (INSERT INTO)
이제 "Add" 버튼을 누르면, 데이터를 저장하는 기능을 구현하겠습니다. 데이터를 추가할 때는INSERT INTO문을 사용합니다. 그런데 예제에서는 테이블 내에 단 하나의 레코드만 저장하므로, 저장되어 있는 레코드를 지우고 나서 새로 추가하도록 만들겠습니다.
SQLiteDatabase.openOrCreateDatabase()함수를 호출하여 데이터베이스를 열고,SQLiteDatabase클래스 객체를 확보한 다음, 데이터베이스를 다루는 함수를 사용하여 데이터를 추가하거나, 수정, 삭제 또는 조회하는 예제를 작성하였습니다.
작성한 예제가,SQLiteDatabase에 대한 사용법을 보여주고 있고, 앱의 동작도 문제가 없지만, 코드 작성에 대하여 몇 가지 개선해야 할 점이 있습니다.
1.1 SQL Helper(SQLiteOpenHelper 클래스) 사용.
예제에서 데이터베이스 파일을 열 때SQLiteDatabase.openOrCreateDatabase()함수를 직접 호출했는데, 안드로이드에서는 이 방법보다SQLiteOpenHelper클래스를 사용하기를 권장하고 있습니다.
SQLiteOpenHelper를 사용하면SQLiteDatabase.openOrCreateDatabase()함수를 사용하여 데이터베이스를 열 때 파일의 경로까지 직접 지정해야 하는 번거로움을 해소할 수 있습니다. 그리고 앱의 기능 수정으로 인해 데이터베이스 테이블의 구조가 바뀌어야 하는 상황에 대해서도 적절히 대처할 수 있는 기반을 제공함으로써 데이터베이스 관리를 용이하게 만들어줍니다.
데이터베이스를 열 때는SQLiteDatabase.openOrCreateDatabase()를 직접 호출하지 말고,SQLiteOpenHelper를 사용하는 것이 좋습니다. 일단SQLiteOpenHelper객체를 생성하고 나면,SQLiteOpenHelper의getWritableDatabase()또는getReadableDatabase()함수를 통해SQLiteDatabase객체를 참조할 수 있습니다.
1.2 계약 클래스 (Contract Class) 사용.
예제에서는 SQL 문장을 만들 때, 테이블 이름(CONTACT_T)과 필드 이름(NO, NAME, ...)을 매번 직접 지정하였습니다.
private void init_tables() {
// 코드 계속 ...
String sqlCreateTbl = "CREATE TABLE IF NOT EXISTS CONTACT_T (" +
"NO " + "INTEGER NOT NULL," +
"NAME " + "TEXT," +
"PHONE " + "TEXT," +
"OVER20 " + "INTEGER" + ")" ;
// ... 코드 계속
}
private void load_values() {
// 코드 계속 ...
String sqlQueryTbl = "SELECT * FROM CONTACT_T" ;
// ... 코드 계속
}
private void save_values() {
// 코드 계속 ...
String sqlInsert = "INSERT INTO CONTACT_T " +
"(NO, NAME, PHONE, OVER20) VALUES (" +
Integer.toString(no) + "," +
"'" + name + "'," +
"'" + phone + "'," +
((isOver20 == true) ? "1" : "0") + ")" ;
// ... 코드 계속
}
기능 수정 또는 확장으로 인해 데이터베이스 구조가 변경되어야 한다면 어떻게 될까요? 그러한 구조 변경으로 인해 테이블 이름 또는 필드 이름이 수정되어야 된다면? 모든 테이블, 필드 이름을 찾아서 수정된 이름으로 변경해야합니다.
테이블이나 필드 이름이 사용되는 곳이 얼마 없으니 상관없지 않을까... 라고 생각할 수도 있습니다. 하지만 테이블과 필드의 갯수가 많아지고 SQL 문장을 조합하는 곳이 많아지면, 이름 수정 작업은 큰 부담으로 다가올 수 밖에 없습니다.
이런 문제를 해결할 수 있는 방법은 계약 클래스(Contract Class)를 사용하는 것입니다.
계약 클래스(Contract Class)는프로그램 개발 과정에서 참조되는 여러 상수들을 정의한 클래스를 말합니다. 이 의미를 데이터베이스에서 적용해보자면, 데이터베이스의 계약 클래스(Contract Class)란, 테이블 이름, 열(Column) 이름, 기능 별 SQL 문장들에 대한 상수 정의를 포함한 클래스를 의미합니다. "계약(Contract)"이라는 단어가 "어떤 행위를 함에 있어 관련된 사람들이 지켜야 할 의무 또는 약속을 문서로 남긴 것"을 의미하듯, 계약 클래스(Contract Class)는 "데이터베이스를 사용함에 있어 개발자가 사용해야 할 여러 정보를 상수로 정의해둔 것"이라고 정리할 수 있습니다.
계약 클래스(Contract Class)를 사용함으로써 얻을 수 있는 장점은 데이터베이스와 관련된 정보를 한 곳에서 관리할 수 있다는 것과, 데이터베이스 구조 또는 이름이 변경될 때의 수정 작업을 최소화할 수 있다는 것입니다.
계약 클래스(Contract Class)를 만들 때는 앱의 패키지 루트에 생성하는 것이 좋습니다. 앱 소스의 어디서든 바로 참조 가능하도록 만드는 것이 좋기 때문입니다. 그리고 계약 클래스(Contract Class)는 인스턴스를 만들 필요가 없기 때문에 생성자의 접근 제한자(Access Modifier)를 private으로 선언합니다.
계약 클래스(Contract Class)에 대한 구체적인 사용 방법은 아래 예제의 ContactDBCtrct 클래스를 참고하시기 바랍니다.
2. 개선된 SQLite 데이터베이스 사용 예제
예제의 화면 레이아웃은 이전 예제에서 사용한 레이아웃을 수정없이 그대로 사용하겠습니다.
2.1 MainActivity의 레이아웃 작성.
MainActivity의 레이아웃은 이전 예제에서 사용한 코드를 그대로 적용합니다.
[STEP-1] "activity_main.xml" - MainActivity의 레이아웃 XML 코드 작성.
코드 개선의 첫 번째 과정으로, 데이터베이스와 관련된 상수를 포함하는 계약 클래스(Contract Class)를 추가합니다. 계약 클래스(Contract Class)에 어떤 정보를 포함시킬 것인지는 개발자의 결정에 따릅니다. 단순히 데이터베이스 이름, 테이블 이름, 열(Column) 이름 정도의 상수만 포함시킬 수 있고, 더 나아가 단순 조합 SQL 문 또는 기능 별 컬럼과 파라미터 조합을 리턴하는 클래스 정적 메소드까지 선언할 수도 있습니다.
예제에서 사용하는 계약 클래스는 테이블 이름, 열(Column) 이름, 단순 조합 SQL 문 정도만 포함합니다.
[STEP-2] "ContactDBCtrct.java" - 데이터베이스 Contact를 위한 계약 클래스.
public class ContactDBCtrct {
private ContactDBCtrct() {} ;
public static final String TBL_CONTACT = "CONTACT_T" ;
public static final String COL_NO = "NO" ;
public static final String COL_NAME = "NAME" ;
public static final String COL_PHONE = "PHONE" ;
public static final String COL_OVER20 = "OVER20" ;
// CREATE TABLE IF NOT EXISTS CONTACT_T (NO INTEGER NOT NULL, NAME TEXT, PHONE TEXT, OVER20 INTEGER)
public static final String SQL_CREATE_TBL = "CREATE TABLE IF NOT EXISTS " + TBL_CONTACT + " " +
"(" +
COL_NO + " INTEGER NOT NULL" + ", " +
COL_NAME + " TEXT" + ", " +
COL_PHONE + " TEXT" + ", " +
COL_OVER20 + " INTEGER" +
")" ;
// DROP TABLE IF EXISTS CONTACT_T
public static final String SQL_DROP_TBL = "DROP TABLE IF EXISTS " + TBL_CONTACT ;
// SELECT * FROM CONTACT_T
public static final String SQL_SELECT = "SELECT * FROM " + TBL_CONTACT ;
// INSERT OR REPLACE INTO CONTACT_T (NO, NAME, PHONE, OVER20) VALUES (x, x, x, x)
public static final String SQL_INSERT = "INSERT OR REPLACE INTO " + TBL_CONTACT + " " +
"(" + COL_NO + ", " + COL_NAME + ", " + COL_PHONE + ", " + COL_OVER20 + ") VALUES " ;
// DELETE FROM CONTACT_T
public static final String SQL_DELETE = "DELETE FROM " + TBL_CONTACT ;
}
2.3 SQLiteOpenHelper 상속 및 구현.
코드 개선의 두 번째 과정으로,SQLiteOpenHelper를 상속하는 클래스를 만듭니다.
[STEP-3] "ContactDBHelper.java" - SQLiteOpenHelper를 상속한 Helper 클래스 정의
public class ContactDBHelper extends SQLiteOpenHelper {
public static final int DB_VERSION = 1 ;
public static final String DBFILE_CONTACT = "contact.db" ;
public ContactDBHelper(Context context) {
super(context, DBFILE_CONTACT, null, DB_VERSION);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(ContactDBCtrct.SQL_CREATE_TBL) ;
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// db.execSQL(ContactDBCtrct.SQL_DROP_TBL) ;
onCreate(db) ;
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// onUpgrade(db, oldVersion, newVersion);
}
}
2.4 데이터베이스 Helper 클래스 변수 선언 및 초기화.
SQLiteOpenHelper클래스를 상속한 ContactDBHelper를 사용하기 위해 MainActivity에 클래스 변수로 선언하고, 초기화 과정을 수행합니다. 초기화 과정은 매우 간단합니다. ContactDBHelper 클래스의 인스턴스를 만드는 것만으로 수행되니까요.
[STEP-4] "MainActivity.java" - ContactDBHelper 변수 선언 및 초기화.
public class MainActivity extends AppCompatActivity {
ContactDBHelper dbHelper = null ;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 코드 계속 ...
// init sqlite db helper.
init_tables() ;
// ... 코드 계속
}
private void init_tables() {
dbHelper = new ContactDBHelper(this) ;
}
// ... 코드 계속
}
2.5 앱 실행 시, 테이블 데이터 조회하여 표시. (SELECT)
이제 새로 추가된 ContactDBHelper 클래스 객체를 사용하는 데이터 조회 기능을 작성하겠습니다. 예제에서는SQLiteDatabase.openOrCreateDatabase()함수를 통해 확보한SQLiteDatabase객체를 직접 사용했지만, 여기서는SQLiteOpenHelper클래스에서 제공하는getReadableDatabase()함수를 통해SQLiteDatabase객체를 가져옵니다.
그리고 데이터 조회를 위한 SELECT 문을 직접 만들지 않고, 계약 클래스에 미리 만들어둔 ContactDBCtrct.SQL_SELECT 를 사용합니다.
[STEP-5] "MainActivity.java" - SELECT 문을 통해 데이터 조회 및 표시.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... 코드 계속
// init sqlite db helper.
init_tables() ;
// load values from db.
load_values() ;
// 코드 계속 ...
}
private void load_values() {
SQLiteDatabase db = dbHelper.getReadableDatabase() ;
Cursor cursor = db.rawQuery(ContactDBCtrct.SQL_SELECT, null) ;
if (cursor.moveToFirst()) {
// no (INTEGER) 값 가져오기.
int no = cursor.getInt(0) ;
EditText editTextNo = (EditText) findViewById(R.id.editTextNo) ;
editTextNo.setText(Integer.toString(no)) ;
// name (TEXT) 값 가져오기
String name = cursor.getString(1) ;
EditText editTextName = (EditText) findViewById(R.id.editTextName) ;
editTextName.setText(name) ;
// phone (TEXT) 값 가져오기
String phone = cursor.getString(2) ;
EditText editTextPhone = (EditText) findViewById(R.id.editTextPhone) ;
editTextPhone.setText(phone) ;
// over20 (INTEGER) 값 가져오기.
int over20 = cursor.getInt(3) ;
CheckBox checkBoxOver20 = (CheckBox) findViewById(R.id.checkBoxOver20) ;
if (over20 == 0) {
checkBoxOver20.setChecked(false) ;
} else {
checkBoxOver20.setChecked(true) ;
}
}
}
// 코드 계속 ...
}
2.6 입력 데이터 저장하기. (INSERT INTO)
데이터 조회 기능과 마찬가지로, 데이터베이스 Helper 클래스로부터SQLiteDatabase의 객체를 가져온 다음, INSERT 문을 실행합니다. INSERT 문이 실행되면 데이터베이스의 내용이 변경되기 때문에,SQLiteOpenHelper.getWritableDatabase()함수를 통해SQLiteDatabase객체를 가져와야 합니다.
[STEP-6] "MainActivity.java" - 데이터 저장하기.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... 코드 계속
Button buttonSave = (Button) findViewById(R.id.buttonSave) ;
buttonSave.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
save_values() ;
}
});
// 코드 계속 ...
}
// ... 코드 계속
private void save_values() {
SQLiteDatabase db = dbHelper.getWritableDatabase() ;
db.execSQL(ContactDBCtrct.SQL_DELETE) ;
EditText editTextNo = (EditText) findViewById(R.id.editTextNo) ;
int no = Integer.parseInt(editTextNo.getText().toString()) ;
EditText editTextName = (EditText) findViewById(R.id.editTextName) ;
String name = editTextName.getText().toString() ;
EditText editTextPhone = (EditText) findViewById(R.id.editTextPhone) ;
String phone = editTextPhone.getText().toString() ;
CheckBox checkBoxOver20 = (CheckBox) findViewById(R.id.checkBoxOver20) ;
boolean isOver20 = checkBoxOver20.isChecked() ;
String sqlInsert = ContactDBCtrct.SQL_INSERT +
" (" +
Integer.toString(no) + ", " +
"'" + name + "', " +
"'" + phone + "', " +
((isOver20 == true) ? "1" : "0") +
")" ;
db.execSQL(sqlInsert) ;
}
// 코드 계속 ...
}
2.7 데이터 삭제하기. (DELETE)
삭제하는 코드 또한 개선된 방법을 적용합니다.
[STEP-7] "MainActivity.java" - 데이터 삭제하기.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... 코드 계속
Button buttonClear = (Button) findViewById(R.id.buttonClear) ;
buttonClear.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
delete_values() ;
}
});
// 코드 계속 ...
}
// ... 코드 계속
private void delete_values() {
SQLiteDatabase db = dbHelper.getWritableDatabase() ;
db.execSQL(ContactDBCtrct.SQL_DELETE) ;
EditText editTextNo = (EditText) findViewById(R.id.editTextNo) ;
editTextNo.setText("") ;
EditText editTextName = (EditText) findViewById(R.id.editTextName) ;
editTextName.setText("") ;
EditText editTextPhone = (EditText) findViewById(R.id.editTextPhone) ;
editTextPhone.setText("") ;
CheckBox checkBoxOver20 = (CheckBox) findViewById(R.id.checkBoxOver20) ;
checkBoxOver20.setChecked(false) ;
}
}
3. 예제 실행 화면
예제의 실행 화면은 이전 예제와 동일합니다.
각 필드에 값을 입력하고 "SAVE" 버튼을 누르면, 입력된 값들이 데이터베이스에 저장됩니다. 그리고 앱을 다시 실행하면, 데이터베이스에 저장된 값이 화면에 표시되는 것을 확인할 수 있습니다.