개발과정중 지속적인 모델을 변경(데이터베이스에 적용)을 하고

서비스코드를 작성하면서 지속적인 로직 변경이 있을시 ORM은 유닛테스트와 결합하여 빠른 개발 사이클을 수행할수가 있습니다.

초기단계부터 유닛테스트를 적용하면서 API를 완성하는 시나리오 전략은 중요합니다.


xUnit 테스트 추가

core 프로젝트의 솔류션에서 xUnit테스트를 추가하여 다음을 수행합니다.

  • 테스트 대상이 되는 프로젝트 참조추가하기
  • 기존 프로젝트가 사용하는 외부 라이브러리 누겟을 통해 동일하게 설치

솔류션에서 우클릭후 솔류션용 Nuget 관리로 접근을 하면, 기존 설치항목을 선택하여

다른 프로젝트에 동일하게 의존 라이브러리 설치가 가능합니다.


테스트에 사용된 모델

    [Table("user")]
    public class User
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int UserId { get; set; }

        public List<SocialInfo> SocialInfos { get; set; }

        public Boolean IsSocialActive { get; set; }

        [StringLength(50,MinimumLength = 3)]
        [Required]
        public String NickName { get; set; }

        [StringLength(50, MinimumLength = 3)]
        [Required]
        public String MyId { get; set; }

        [StringLength(50, MinimumLength = 3)]
        [Required]
        public String PassWord { get; set; }

        public DateTime RegDate { get; set; }
        

    }
  • Table : 객체명에 해당하는 테이블명 지정
  • Key : Primary Key
  • GeneratedOption : 자동증가값 전략(DB제공활용가능)
  • Required : Not null
  • StringLength : 문자열길이 제한

최소길이의 제약조건은 DB에는 없으며, 어플리케이션 레벨에서

유효검사를하게됩니다.


    [Table("tokenhistory")]
    public class TokenHistory
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int TokenHistoryId { get; set; }

        [ForeignKey("UserForeignKey")]
        public User User { get; set; }
        
        public String AuthToken { get; set; }

        public DateTime CreateTime { get; set; }

        public DateTime AccessTime { get; set; }

    }

사용자를 외래키(FoeignKey)로 설정함으로(실제는 UserId가 지정됨)

토큰히스토리를 통해 토큰 소유자정보 접근이 가능합니다.


SQLORM

Select u.NickName * from TokenHistory t

join User u u.userid=t.userid where u.Userid=1;


TokenHistory token=_context.TokenHistory.First(t => t.UserId == 1);

token.User.NickName;

위 사용코드는 결국 Join을 사용하는 SQL문을 호출할것입니다.

ORM을 사용하면 SQL문을 작성할 일이 줄어들지만, SQL문을 몰라야됨을 의미하는것은 아닙니다.

Null허용제약조건에서 왜? inner join을 사용하면 안되는지 단순한 SQL문 질문에

바로답하고 머릿속에 그릴수 있어야 올바른 Entity관계를 맺을수가 있습니다.


    public class AccountContent : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<SocialInfo> SocialInfos { get; set; }
        public DbSet<TokenHistory> TokenHistories { get; set; }

        public AccountContent( DbContextOptions<AccountContent> options )
        : base(options)
        {
            
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //제약설정( Fluent API를 통해 확장 설정이 가능합니다 )
            modelBuilder.Entity<TokenHistory>()
                .HasIndex(p => new { p.AuthToken })
                .IsUnique(true);
        }
        
    }
  • HasIndex로 복합키설정및 유일키등 추가적인 제약조건 설정가능합니다.

위 스키마와 테이블은 C# Object정의로만 자동 생성되었습니다. 1 VS N관계를 위해 외래키설정도 되었으며

추가적으로 제약조건도 설정이 되었습니다. 엔티티의 테이블명과 필드명및 제약조건이 수없이 바뀌고 검증을 해야한다고 가정해봅시다.

대부분의 시간을 쿼리작성시간에 허비해야하며, 어플리케이션에 그것을 이용했을때 적합한 설계인지 판단하기까지

아주 오랜시간이 소요됩니다. 개발 초기 프로타잎을 빠르게 만드는것이 1차목적이며 완성도에따라 이것은 바로 서비스화가 가능할수도 있습니다.

ORM을 통해 빠르게 설계하고 빠르게 개선점을 찾는 ,초기 프로젝트의 개발 라이프 사이클을 단축 시키는것이 최종 목표입니다.



서비스 구현하기

    public class AccountService
    {
        private readonly AccountContent _context;

        public AccountService(AccountContent context)
        {
            _context = context;
        }

        public User GetUserByid(int id)
        {
            return _context.Users.First(p => p.UserId == id);
        }

        public String GetAccessToken(string userid,string userpw)
        {
            User accessUser = _context.Users.First(p => p.MyId == userid && p.PassWord == userpw);
            if (accessUser == null)
            {
                throw new Exception("401");
            }
            else
            {
                string token = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
                _context.TokenHistories.Add(new TokenHistory()
                {
                    User = accessUser,
                    AuthToken = token,
                    CreateTime = DateTime.Now,
                    AccessTime = DateTime.Now
                });

                _context.SaveChanges();
                return token;
            }
        }

        public User GetMyInfo(string accessToken)
        {
            User myinfo= _context.TokenHistories.First(p => p.AuthToken == accessToken).User;
            if (myinfo == null) throw new Exception("401");
            myinfo.PassWord = "******";
            return myinfo;
        }

    }

위 샘플은 개선점이 더 있을수 있지만, 기본적으로 인증이후 AccessToken을 할당받고..  할당받은 AccessToken을 통해

인증에 관련된 API를 서비스할수 있다란점이며..., SQL문 작성없이 DB 스토리지에 완벽하게 저장이되면서 작동이 됩니다.


서비스 TestCode 설계하기

  • DBInit : 마지막 설계된 Entity가 DB에 모두 반영이되며 / 샘플데이터가 추가됩니다.
  • AccountControlerTest : 테스트 DB와 연동되어 서비스로직을 실제 테스트합니다. ( 목업 DB를 활용한 패턴도 있음 )


AccountControlerTest
    public class AccountControlerTest
    {
        public static readonly string ConnectionString = "server=localhost;database=db_account;user=psmon;password=db1234";

        public AccountControlerTest()
        {
            InitContext();
        }

        private AccountContent _context;

        internal static int PrepareTestData()
        {
            var builder = new DbContextOptionsBuilder<AccountContent>()
                .UseMySql(AccountControlerTest.ConnectionString);
            var context = new AccountContent(builder.Options);

            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();
            var users = Enumerable.Range(1, 10)
                .Select(i => new User { MyId = "TestID" + i, PassWord = "TEST123" + i, NickName = "Mynick" + i, RegDate = DateTime.Now, IsSocialActive = false });

            context.Users.AddRange(users);
            context.SaveChanges();           
            return context.Users.Count(t => t.IsSocialActive == false);
        }

        protected void InitContext()
        {
            var builder = new DbContextOptionsBuilder<AccountContent>()
                .UseMySql(AccountControlerTest.ConnectionString);

            var context = new AccountContent(builder.Options);            
            _context = context;            
        }

        [Fact]
        public void TestUserCntChk()
        {
            var count = _context.Users.Count(t => t.IsSocialActive==false);
            Assert.Equal(10, count);
        }

        [Fact]
        public void TokenTest1()
        {
            var service = new AccountService(_context);           
            var accessToken = service.GetAccessToken("TestID1", "TEST1231");
            User myInfo = service.GetMyInfo(accessToken);
            Assert.Equal("Mynick1", myInfo.NickName);           
            //TestID1 TEST1231
        }
        
        [Fact]
        public void TestUserActive()
        {
            bool expected = false;
            var controller = new AccountControler(_context);
            User result = controller.GetUserByid(1);
            Assert.Equal(expected, result.IsSocialActive);
        }
    }

위 코드는 우리가 작성한 서비스코드를 확인하고, 지속적으로 개선활동을 하는시간을 줄여줍니다. 

쿼리작성과, 호출된 결과물의 Entity와 맵핑을 시키는 시간도 비약적으로 줄일수 있습니다.

우리가 정의한 C# Object Entity가 곧 테이블의 Entity 가 되기 때문입니다.


GIT : http://git.webnori.com/projects/OPPJ/repos/accountapi/browse/accountapi_test




  • No labels