졸업 작품_Easy Tag-1(API)

서론

학부 4학년 1학기(20.03~20.06)기간 동안 졸업 작품을 준비하고 전시했는데, 졸업 작품에서 내가 맡아서 개발한 부분에 대해 정리하여 게시하고자 한다. 졸업 작품은 조별로 프로젝트를 진행하는 형태로 최종 성과물을 완성하여 작품 전시전을 여는 형태로 진행한다.

우리 조는 공간DB와 QR코드를 활용한 기자재 관리 어플리케이션 라는 주제로 졸업작품을 진행하였다. 간단히 설명하자면, 기존의 불편하고 직관적이지 않은 기자재 관리 방식에서 벗어나 QR코드를 활용하여 각 기자재와 호실(방)에 대해 QR코드를 부착하여, 기자재 관리 시 QR코드를 스캔하여 DB연동을 통해 기자재 관리가 이루어지는 어플리케이션을 개발한다. 앱의 개발 배경과 목적 등에 대한 자세한 설명은 졸업작품 온라인 전시 링크를 통해 보다 자세히 확인할 수 있다.

졸업작품 개발에서 내가 맡은 부분은 API 개발과 어플리케이션의 실내 지도 구현을 담당하였는데, 이에 대한 내용을 작성하고자 한다.

개발 과정

API

스마트폰과 데이터베이스를 연동하기 위해서는 모바일 앱-DB간의 연동 작업이 필요한데, 이는 안드로이드에서 직접적으로 DB를 구축하여 연동하는 방식이 아닌, 별도의 웹 서버에 DB를 구축하고 이를 연동할 수 있는 API를 개발하여, 모바일 환경에서 API를 호출하여 해당 DB와 연동이 이루어지도록 설계하였다. 이는 앱에 사용되는 DB의 구조가 다소 복잡하고 데이터의 양이 많기 때문에 모바일 DB에서 처리하는데 한계가 있으므로 별도의 웹 서버를 구축하여 이를 사용하였다.

API의 경우 C#의 ASP.NET CORE API를 통해 개발하였다. 공간DB 연구실의 학부연구생으로 업무를 진행하면서 ASP.NET CORE API를 사용하여 개발한 경험이 있었기 때문에, 해당 API를 채택하였다.

ASP.NET CORE API는 MVC 형태로 구현된 REST API 구조이다. 이는 도메인에서 DB와 연동하여 데이터에 대한 조회, 수정, 삭제 등에 관한 CRUD 작업을 Model-View-Controller로 분리하여 처리하는 API형태이다. 따라서 API 설계 시 이러한 MVC 구조에 대한 설계가 필요하다.

먼저, Model의 경우 쿼리문의 결과로 반환되는 각 테이블의 어트리뷰트들에 해당한다. 따라서 데이터베이스에 존재하는 각 테이블들에 대한 Model을 구축한다.

namespace RoomApi.Models
{
    public class Equipment:BaseEntity
    {
        [Key]
        public double EquipNo { get; set; }
        public int CauseNo { get; set; }
        public string Name_KR { get; set; }
        public string Name_ENG { get; set; }
        public string ItemName { get; set; }
        public bool IsDisuse { get; set; }
        public DateTime InDate { get; set; }
        public int Price { get; set; }
        public string Type { get; set; }
        public int LifeYear { get; set; }
        public string Status { get; set; }
        public DateTime QR_Date { get; set; }
        public string ETC { get; set; }
        public int RoomID { get; set; }
        public string UserID { get; set; }
        public string AdminID { get; set; }
    }
}

위 코드는 Equipment 클래스에 대한 코드로 데이터베이스에서 기자재가 저장되는 테이블인 Equipment에 대한 Model의 클래스이다. 클래스는 Equipment 테이블에 존재하는 각 어트리뷰트들이 프로퍼티로 정의되며, 컨트롤러에서 Equipment 테이블에서 쿼리문을 실행하여 결과값을 받을 때 Equipment 클래스의 객체로 각각의 프로퍼티를 통해 어트리뷰트 값들이 지정되어 반환된다.

다음으로, 이러한 Model들을 바탕으로 쿼리문을 통해 DB에 접근하여 작업을 수행하는 과정이 필요하다. 이 때, 이를 위해 MVC Pattern의 Repository Pattern을 활용한다. Repository Pattern에 대한 공식적인 정의는 다음과 같다.

“Repostory는 도메인과 데이터 매핑 계층을 중재하여 메모리 내 도메인 개체 컬렉션과 같은 역할을 한다. 클라이언트 개체는 선언적으로 쿼리 규격을 구성하고 이를 충족시키기 위해 repository에 제출한다. 개체는 단순 개체 모음에서 가능하듯이 repository에 의해 캡슐화된 매핑 코드는 뒤에서 적절한 작업을 수행한다. 개념적으로 repository는 데이터 저장소에 유지된 개체 집합과 그 위에서 수행된 작업을 캡슐화하여 지속성 계층에 대한 객체 지향적인 뷰를 제공한다. 또한, repository는 도메인과 데이터 매핑 계층간의 완전한 분리 및 단방향 종속성을 달성한다는 목표를 지원한다.”

정리하자면, repository는 데이터 저장소(DB)와 도메인을 캡슐화된 매핑 코드를 통해 중재하는 역할을 한다. 이를 통해 API에서 DB와의 연동 작업을 체계적이고 수월하게 할 수 있는 장점이 있다.

namespace RoomApi.Repository
{
    public class EquipmentRepository : IRepository<Equipment>
    {
        private string connectionString;
        public EquipmentRepository(IConfiguration configuration)
        {
            connectionString = configuration.GetValue<string>("DBInfo:ConnectionString");
        }

        internal IDbConnection Connection
        {
            get
            {
                return new NpgsqlConnection(connectionString);
            }
        }

        public void Add(Equipment item)
        {
            using (IDbConnection dbConnection = Connection)
            {
                dbConnection.Open();
                dbConnection.Execute("INSERT INTO Equipment (EquipNo, CauseNo, Name_KR, Name_ENG, ItemName, IsDisuse, Indate, Price, Type, LifeYear, Status, QR_Date, ETC, RoomID, UserID, AdminID) " +
                    "VALUES(@equipNo, @causeNo, @name_KR, @name_ENG, @itemName, @isDisuse, @indate, @price, @type, @lifeYear, @status, @qr_Date, @eTC, @roomID, @userID, @adminID)", item);
            }

        }

        public IEnumerable<Equipment> FindAll()
        {
            using (IDbConnection dbConnection = Connection)
            {
                dbConnection.Open();
                return dbConnection.Query<Equipment>("SELECT * FROM Equipment");
            }
        }

        public Equipment FindByID(double equipno)
        {
            using (IDbConnection dbConnection = Connection)
            {
                dbConnection.Open();
                return dbConnection.Query<Equipment>("SELECT * FROM equipment WHERE EquipNo = @Equipno", new { Equipno = equipno }).FirstOrDefault();
            }
        }

        public IEnumerable<Equipment> FindByCond(Condtion condt)
        {
            using (IDbConnection dbConnection = Connection)
            {
                dbConnection.Open();
                return dbConnection.Query<Equipment>("SELECT * FROM equipment WHERE " + condt.Condt);
            }
        }

        public void Remove(Equipment item)
        {
            using (IDbConnection dbConnection = Connection)
            {
                dbConnection.Open();
                dbConnection.Execute("DELETE FROM Equipment WHERE EquipNo=@Equipno", item);
            }
        }

        public void Update(Condtion condt)
        {
            using (IDbConnection dbConnection = Connection)
            {
                dbConnection.Open();
                dbConnection.Query("UPDATE Equipment " + condt.Condt); ;
            }
        }
    }
}

위 코드는 Repository에 대한 예시로 앞서 설계한 Equipment(기자재)에 대한 Repository 코드이다. 기본적으로 Repository는 객체에 대한 Add, Remove, Update 등과 같은 메서드들이 정의된 IRepository 인터페이스를 상속받는 구조이다. 위 Repository를 보면 Equipment 테이블에 대한 쿼리문을 보내고, 리턴값이 있는 경우 이전에 정의한 Model 객체 Equipment의 타입으로 전달받아 이를 리턴하는 메서드로 구성되어 있다. 따라서 Repository에 정의된 메서드들을 통해 데이터베이스에 대한 조회, 수정, 삭제 등의 작업을 수행할 수 있다.

또한, 위 메서드들 중 FindByCond와 update와 같은 메서드를 보면 condt 객체를 사용하여 조건문을 수행하는 것을 볼 수 있다. 이는, 기본적인 형태의 쿼리문 외에도 동적으로 쿼리문을 할당하여 다양한 방식으로 연동작업을 수행하기 위함이다. Condt 객체의 경우 사용자가 정의한 조건문을 텍스트 형태로 갖는 객체이다.

이렇게 정의된 Repository는 Controller에서 주소 명명 규칙에 따라 해당 Repositroy의 메서드를 호출하여 DB연동 작업이 수행되도록 구현할 수 있다. 아래 코드는 Equipment(기자재)에 해당하는 Controller의 코드이다.

namespace RoomApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class RoomController : Controller
    {
        private readonly EquipmentRepository equipmentRepository;
        ...

        public RoomController(IConfiguration configuration)
        {
            equipmentRepository = new EquipmentRepository(configuration);
            ...
        }        

        [HttpGet("equipment")]
        public IActionResult GetAllEquipment()
        {
            return new OkObjectResult(equipmentRepository.FindAll());
        }

        [HttpGet("equipment/{id}")]
        public IActionResult GetOneEquipment(double id)
        {
            return new OkObjectResult(equipmentRepository.FindByID(id));
        }

        [HttpPost("equipment_add")]
        public void AddEquipment(Equipment item)
        {
            equipmentRepository.Add(item);
        }

        [HttpPost("equipment_delete")]
        public void ReomveEquipment(Equipment item)
        {
            equipmentRepository.Remove(item);
        }

        [HttpPost("equipment_update")]
        public void UpdateEquipment(Condtion condt)
        {
            equipmentRepository.Update(condt);
        }

        [HttpPost("equipment_condt")]
        public IActionResult GetEquipByCondt(Condtion condt)
        {
            return new OkObjectResult(equipmentRepository.FindByCond(condt));
        }
        ...

    }
}

기본적으로 컨트롤러에는 도메인에서 각 메서드에 접근할 수 있는 주소 규칙이 정의되어 있다. 루트값인 api/[Controller]는 기본 주소 형태이며 API에 할당된 IP 주소 또는 도메인에 이어서 주소가 연결된다. 예를 들어, 로컬 서버를 통해 주소로 접근하는 경우 “http://localhost:0000/api/room”이 기본 주소가 된다.

GetAllEquipment 메서드의 경우 [HttpGet(“equipment”)]으로 주소가 설정되어 있다. 이는 GET 방식의 HTTP 통신으로 “기본 주소/equipment”로 접근할 때 해당 메서드를 호출한다는 의미이다. 이 경우 주소는 “http://localhost:0000/api/room/equipment” 가 된다. 메서드는 EquipmentRepository의 Findall 메서드를 호출하도록 정의되어 있으며 해당 메서드는 "SELECT * FROM equipment"쿼리문을 보내 결과값으로 equipment 객체들을 넘겨받는 메서드이다. 전달받은 Equipment 객체들은 최종적으로 OkObjectResult 객체를 통해 json형태로 출력된다. 결과 예시는 아래와 같이 모든 기자재에 대한 항목이 조회된다.

[
    {
        "equipNo":201056101703003,
        "causeNo":2110,"name_KR":"책상",
        "name_ENG":"Desks",
        "itemName":"책상, 크로바가구, CLV-1480T, 1400×800×720mm",
        "isDisuse":false,
        "inDate":"2010-12-23T00:00:00",
        "price":125000,
        "type":"사무자재",
        "lifeYear":8,
        "status":"정상",
        "qR_Date":"2020-04-24T00:00:00","etc":"기타",
        "roomID":2067,
        "userID":"abcd1234",
        "adminID":"uosspatial"
    },...
]

또한, GetOneEquipment 메서드와 같이 사용자가 임의의 매개변수를 입력하여 특정 값에 해당하는 쿼리문을 질의할 수도 있다. 예를 들어, 기자재 항목 중 기자재의 고유 키인 기자재 고유 번호가 “1234”에 해당하는 항목을 조회하는 경우, 주소의 {id} 부분에 해당 번호를 넣어서 GetOneEquipment 메서드를 통해 번호를 쿼리문에 넣어 고유 번호가 “1234”인 기자재 항목 만을 넘겨받을 수 있다. 이 경우, 주소는 “http://localhost:0000/api/room/equipment/1234”가 된다.

Get 방식 뿐만 아니라 POST방식으로도 메서드를 정의할 수 있다. 앞서 설명한 바와 같이, Condt를 통해 임의의 조건문을 할당하여 동적 쿼리를 수행하기 위해서는 Condt 객체의 조건문에 텍스트를 할당해야 하는데, 이는 POST 방식에서 json 형태로 condt 값을 전달하여 특정 쿼리를 수행할 수 있다.

예를 들어, Equipment 테이블에서 기자재의 상태를 의미하는 status 어트리뷰트가 “정상”인 항목만을 조회한다고 가정하자. 이 경우, 쿼리문은 "SELECT * FROM Equipment WHERE STATUS='정상'"이 될 것이다. 하지만, 이러한 조건문에 대한 쿼리문을 사전에 사전에 모두 일일이 정의할 수 없기 때문에, WHERE 절 뒤에오는 조건문의 내용을 사용자가 직접 전달하는 형태로 정의해야 한다. 따라서, 이를 수행하는 메서드를 선언하고 조건문을 String 형태의 매개변수로 전달받아 쿼리문에 추가하도록 정의한다.

위 Equipment Repository의 FindByCond 메서드를 보면 매개변수로 Condt 객체를 전달받도록 정의되어 있다. 이는, String 프로퍼티인 condt를 갖는 객체 Condt를 전달받아 쿼리문에 특정 조건문을 추가할 수 있도록 하기 위함이다. 다시 컨트롤러 부분을 보면 GetEquipByCondt 메서드의 주소가 [HttpPost(“equipment_condt”)]으로 선언되어 있다. 이는 POST방식의 HTTP 통신으로 해당 주소로 데이터를 함께 전송하여 메서드가 실행되는데, 전송되는 데이트는 다음과 같은 json형태를 이루어야 한다.

{"condt":"WHERE STATUS='정상'"}

따라서 해당 조건문이 텍스트 형태로 전달되어 repository의 해당 메서드의 쿼리문으로 들어가 STAUTS 값이 정상인 항목만 조회할 수 있다.

이렇게 연동에 필요한 다양한 형태의 메서드를 정의하고 이를 GET, POST 등의 방식으로 호출하여 모바일 환경(안드로이드)에서도 웹 서버의 DB연동 작업이 이루어질 수 있다.

다음 글(Mapbox Android SDK)