잊지 않겠습니다.

Spring MVC를 이용한 개발에서 가장 멋진 일은 MockMvc를 이용한 테스트다. 특히 Controller의 사용자 Scenario를 짜고, 그 Sceneario의 결과를 테스트 해보는것은 너무 재미있는 일이다. 그런데, Spring MVC에 Spring Security를 적용한 후에 인증에 대한 테스트를 하기 위해서는 다음의 간단한 절차를 거쳐야지 된다. 


1. MockMvc에 Spring Security Filter를 적용해야지 된다.

2. Login 절차를 통과한 사용자를 만들어내야지 된다. - MockHttpSession을 이용한다. 


이와 같은 과정을 조금 단순화하기 위해서 간단한 TestSupport 객체를 만들어봤다. Utility 객체이기 때문에, 하는 일들은 매우 단순하다. 

1. MockMvc에 Spring Security Filter를 적용한 후, Return시킨다.

2. Digest 인증키값을 만들어낼 수 있다.

3. Basic 인증키값을 만들어낼 수 있다.

4. Form 인증이 반영된 MockHttpSession 값을 만들어낼 수 있다.


Helper class 코드는 다음과 같다.


/**
 * User: ykyoon
 * Date: 11/18/13
 * Time: 7:13 PM
 * Spring Security Filter 적용 및 인증 지원을 위한 Test Helper Class
 */
public class AuthorizedControllerHelper {

    /**
     * MockMvc 생성 코드
     * @param context WebApplicationContext
     * @return Spring Security Filter가 적용된 MockMvc 객체
     * @throws Exception
     */
    public static MockMvc getSecurityAppliedMockMvc(WebApplicationContext context) throws Exception {
        DelegatingFilterProxy delegateProxyFilter = new DelegatingFilterProxy();
        MockFilterConfig secFilterConfig = new MockFilterConfig(context.getServletContext(),
                BeanIds.SPRING_SECURITY_FILTER_CHAIN);
        delegateProxyFilter.init(secFilterConfig);

        return MockMvcBuilders.webAppContextSetup(context).addFilter(delegateProxyFilter).build();
    }

    public static final String AUTH_HEADER = "Authorization";

    /**
     * Basic 인증 문자열 생성
     * @param username 사용자 이름
     * @param password 비밀번호
     * @return Basic XXXX 형태의 Authorization 문자열
     * @throws Exception
     */
    public static final String buildBasicAuthHeaderValue(String username, String password) throws Exception {
        String authHeaderFormat = "Basic ";
        String encodingRawData = String.format("%s:%s", username, password);
        String encodingData = authHeaderFormat + new String(Base64.encode(encodingRawData.getBytes("utf-8")));
        return encodingData;
    }

    /**
     * Digest 인증 문자열 생성
     * @param mvc MockMvc. Digest의 경우, 한번의 Request를 통해 서버의 nonce값을 얻어내야지 된다.
     *            Spring Security의 EntryEndPoint의 설정이 DigestAuthenticationFilter로 되어있어야지 된다.
     * @param username 사용자 이름
     * @param password 비밀번호
     * @param uri 호출할 URI
     * @param method HttpRequestMethod : GET, POST, PUT, DELETE
     * @return Digest 인증 문자열
     * @throws Exception
     */
    public static String buildDigestAuthenticateion(MockMvc mvc, String username,
                                                    String password,
                                                    String uri, String method) throws Exception {
        MvcResult mvcResult = null;
        if(method.equals("GET")) {
            mvcResult = mvc.perform(get(uri)).andDo(print()).andReturn();
        } else if(method.equals("POST")) {
            mvcResult = mvc.perform(post(uri)).andDo(print()).andReturn();
        } else if(method.equals("PUT")) {
            mvcResult = mvc.perform(put(uri)).andDo(print()).andReturn();
        } else if(method.equals("DELETE")) {
            mvcResult = mvc.perform(delete(uri)).andDo(print()).andReturn();
        }
        String authHeader = mvcResult.getResponse().getHeader("WWW-Authenticate");
        String[] authHeaderItemStrings = authHeader.split(",\\s");
        Map<String, String> authItems = new HashMap<>();
        Pattern keyAndItemPattern = Pattern.compile("(Digest\\s)?(?<key>[^=]+)=\"(?<value>[^\"]+)\"");
        for(int i = 0 ; i < authHeaderItemStrings.length; i++) {
            Matcher matcher = keyAndItemPattern.matcher(authHeaderItemStrings[i]);
            assertThat(matcher.find(), is(true));
            String key = matcher.group("key");
            String value = matcher.group("value");
            authItems.put(key, value);
        }
        Assert.assertNotNull(authItems.get("realm"));
        Assert.assertNotNull(authItems.get("nonce"));
        Assert.assertNotNull(authItems.get("qop"));

        String ha1 = DigestUtils.md5DigestAsHex(String.format("%s:%s:%s", username, authItems.get("realm"), password).getBytes("UTF-8"));
        String ha2 = DigestUtils.md5DigestAsHex(String.format("%s:%s", method, uri).getBytes("UTF-8"));
        String cnonce = calculateNonce();
        String totalString = String.format("%s:%s:00000001:%s:%s:%s",
                ha1, authItems.get("nonce"), cnonce, authItems.get("qop"), ha2);
        String response = DigestUtils.md5DigestAsHex(totalString.getBytes("UTF-8"));

        String clientRequest = String.format("Digest username=\"%s\",", username) +
                String.format("realm=\"%s\",", authItems.get("realm")) +
                String.format("nonce=\"%s\",", authItems.get("nonce")) +
                String.format("uri=\"%s\",", uri) +
                String.format("qop=%s,", authItems.get("qop")) +
                "nc=00000001," +
                String.format("cnonce=\"%s\",", cnonce) +
                String.format("response=\"%s\"", response);

        return clientRequest;
    }

    /**
     * Form인증을 위한 MockHttpSession 반환 함수
     * @param context WebApplicationContext
     * @param username 사용자 이름
     * @return Spring Security Attribute가 적용된 MockHttpSession 값
     * @throws Exception
     */
    public static MockHttpSession buildSecuritySession(WebApplicationContext context, String username) throws Exception {
        MockHttpSession session = new MockHttpSession();
        SecurityContext securityContext = buildFormAuthentication(context, username);
        session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext);
        return session;
    }

    /**
     * Spring Security Context 얻어내는 내부 함수
     * @param context WebApplicationContext
     * @param username 사용자 이름
     * @return SecurityContext
     * @throws Exception
     */
    private static SecurityContext buildFormAuthentication(WebApplicationContext context, String username) throws Exception {
        UserDetailsService userDetailsService = (UserDetailsService) context
                .getBean(BeanIds.USER_DETAILS_SERVICE);
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                userDetails,
                userDetails.getPassword(),
                userDetails.getAuthorities());
        SecurityContext securityContext = new SecurityContext() {
            private static final long serialVersionUID = 8611087650974958658L;
            private Authentication authentication;

            @Override
            public void setAuthentication(Authentication authentication) {
                this.authentication = authentication;
            }

            @Override
            public Authentication getAuthentication() {
                return this.authentication;
            }
        };
        securityContext.setAuthentication(authToken);
        return securityContext;
    }

    /**
     * Digest인증시에 사용되는 cnonce 값을 생성하는 내부 함수
     * @return
     * @throws UnsupportedEncodingException
     */
    private static String calculateNonce() throws UnsupportedEncodingException {
        Date d = new Date();
        SimpleDateFormat f = new SimpleDateFormat("yyyy:MM:dd:hh:mm:ss");
        String fmtDate = f.format(d);
        Random rand = new Random(100000);
        Integer randomInt = rand.nextInt();
        return DigestUtils.md5DigestAsHex((fmtDate + randomInt.toString()).getBytes("UTF-8"));
    }
}

Spring Security가 반드시 적용된 Test에서만 사용가능하다. Helper를 이용한 Test code는 다음과 같다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { DomainConfiguration.class, SecurityConfiguration.class, ControllerConfiguration.class })
@WebAppConfiguration
public class AdminNoticeControllerTest {
    @Autowired
    private WebApplicationContext context;
    private MockMvc mvc;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private NoticeService noticeService;

    @Before
    public void setUp() throws Exception {
        assertThat(context, is(not(nullValue())));
        mvc = AuthorizedControllerHelper.getSecurityAppliedMockMvc(context);
    }

    @Test
    public void getAllNotices() throws Exception {
        MockHttpSession session = AuthorizedControllerHelper.buildSecuritySession(context, "ykyoon");
        MvcResult result = mvc.perform(get(AdminNoticeController.API_ADMIN_NOTICES)
                .param("pageIndex", "0")
                .param("pageSize", "10").session(session))
                .andExpect(status().isOk())
                .andReturn();
        checkResultData(result);
    }

    @Test
    public void hideNotice() throws Exception {
        final Page<Notice> notices = noticeService.getNotices(0, 10, false);
        Notice notice = notices.getContent().get(0);
        MockHttpSession session = AuthorizedControllerHelper.buildSecuritySession(context, "ykyoon");
        MvcResult result = mvc.perform(post(AdminNoticeController.API_ADMIN_NOTICE_HIDE)
                    .param("noticeId", notice.getId().toString())
                    .session(session))
                .andExpect(status().isOk())
                .andDo(print())
                .andReturn();
        checkResultData(result);
    }

    @Test
    public void editNotice() throws Exception {

    }

    @Test
    public void showNotice() throws Exception {
        final Page<Notice> notices = noticeService.getNotices(0, 10, true);
        Notice notice = notices.getContent().get(0);
        MockHttpSession session = AuthorizedControllerHelper.buildSecuritySession(context, "ykyoon");
        MvcResult result = mvc.perform(post(AdminNoticeController.API_ADMIN_NOTICE_SHOW)
                .param("noticeId", notice.getId().toString())
                .session(session))
                .andExpect(status().isOk())
                .andDo(print())
                .andReturn();
        checkResultData(result);
    }

    private void checkResultData(MvcResult result) throws Exception {
        ResultData r = objectMapper.readValue(result.getResponse().getContentAsString(), ResultData.class);
        assertThat(r.isOk(), is(true));
    }
}


Posted by Y2K
,

구성된 project는 아주 기본적인 Hibernate를 이용한 ORM 구성을 가지게 된다. 

추가로, Transaction과 Connection Polling을 구성하도록 한다. 


1. Transaction의 추가


<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-tx</artifactId>

<version>3.1.2.RELEASE</version>

</dependency>


<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-aop</artifactId>

<version>3.1.2.RELEASE</version>

</dependency>


spring trasaction을 위한 jar와 aop를 위한 jar를 추가한다.

생성된 PersonService의 interface를 선언한다. 

public interface IPersonService {
Person add(String firstName, String lastName, Double money);
List<Person> getAll();
Person get(Integer id);
Person edit(Integer id, String firstName, String lastName, Double money);
void delete(Integer id);
}

PersonService에 interface구현을 추가하고, @Transaction annotation을 붙인다. 
구현된 PersonService는 다음과 같다.

@Service("personService")
@Transactional
public class PersonService implements IPersonService {
@Resource
private SessionFactory sessionFactory;
@SuppressWarnings("unchecked")
public List<Person> getAll() {
Session session = sessionFactory.getCurrentSession();
List<Person> persons = (List<Person>) session.createCriteria(Person.class).list();
return persons;
}
public Person get(Integer id) {
Session session = sessionFactory.getCurrentSession();
return (Person) session.get(Person.class, id);
}
public Person add(String firstName, String lastName, Double money) {
Person person = new Person();
person.setFirstName(firstName);
person.setLastName(lastName);
person.setMoney(money);
Session session = sessionFactory.getCurrentSession();
session.save(person);
return person;
}
public Person edit(Integer id, String firstName, String lastName, Double money) {
Person person = get(id);
Session session = sessionFactory.getCurrentSession();
person.setFirstName(firstName);
person.setLastName(lastName);
person.setMoney(money);
session.update(person);
return person;
}
public void delete(Integer id) {
Person person = get(id);
Session session = sessionFactory.getCurrentSession();
session.delete(person);
}
}

위 코드와 기존코드의 가장 큰 차이는 sessionFactory.getCurrentSession()이다. 기존 코드는 sessionFactory.openSession()을 사용하지만, transaction을 사용하는 Service는 sessionFactory.getCurrentSession()이 된다. 그 이유는 다음과 같다. 

Transaction annotation을 사용하는 경우에 annotation에 의해서 다음과 같은 코드로 변경이 되어서 실행이 되는것과 동일해진다. 


public Person edit(Integer id, String firstName, String lastName, Double money) {

Session s = sessionFactory.openSession();
Transaction transaction = s.beginTransaction();

Person person = get(id);
Session session = sessionFactory.getCurrentSession();
person.setFirstName(firstName);
person.setLastName(lastName);
person.setMoney(money);
session.update(person);

transaction.commit();
return person;
}

Transaction AOP에 의해서 항시 openSession, beginTransaction 이 실행되는것과 동일하게 구성이 되게 된다. 그리고, method의 종료시에 transaction의 commit가 자동으로 이루어지기 때문에 Transaction이 구현되는 서비스에서는 sessionFactory에서 getCurrentSession을 통해서 transaction이 적용된 session을 얻어내야지된다.

마지막으로 spring bean annotation설정을 위해서 spring-context.xml에 다음과 같은 내용을 추가하도록 한다.
<!-- sessionFactory에 transaction 구성 -->
<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager" 
                         p:sessionFactory-ref="sessionFactory" />
<tx:annotation-driven transaction-manager="transactionManager" />
<!-- sessionFactory 구성 -->

테스트를 구동하고, 정상적으로 동작하는지 확인한다. 



2. Connection Pool의 추가


hibernate에서 주로 사용하는 c3p0를 connection pool로 사용하도록 한다. c3p0는 사용하기 쉽고, 가벼워서 WAS에서 제공하는 connection pool을 사용하지 않는 경우에 주로 추천되는 connection pool이다. 


c3p0 dependency를 pom.xml에 다음과 같이 추가한다.


<dependency>

<groupId>org.hibernate</groupId>

<artifactId>hibernate-c3p0</artifactId>

<version>4.1.7.Final</version>

</dependency>



spring-context에서 기존 dataSource를 제거하고 c3p0에서 제공하는 Pooling DataSource로 대체한다. 

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close"
p:driverClass="${app.jdbc.driverClassName}"
p:jdbcUrl="${app.jdbc.url}"
p:user="${app.jdbc.username}"
p:password="${app.jdbc.password}"
p:acquireIncrement="5"
p:idleConnectionTestPeriod="60"
p:maxPoolSize="10"
p:maxStatements="50"
p:minPoolSize="5"/>


테스트를 구동하고, 정상적으로 테스트를 통과하면 완료된다.


일단, 기본 WEB Application을 개발할 수 있는 환경을 만들었지만, 하나 빼먹은 것이 계속해서보인다.; 

그것은 바로... Log!!! 


3. Log의 추가


요즘 java의 추세인 logback와 slf4j를 사용하도록 한다.; 일단 log4j보다 속도가 좋고, 거기에 log4j와 동일한 방법으로 사용이 가능하다는 장점때문에 logback를 slf4j를 이용해서 facade pattern으로 사용하게 된다. 


pom.xml에 다음을 추가한다. 

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>slf4j-api</artifactId>

<version>${slf4j.version}</version>

</dependency>

<dependency>

<groupId>ch.qos.logback</groupId>

<artifactId>logback-classic</artifactId>

<version>${logback.version}</version>

</dependency>


<dependency>

<groupId>ch.qos.logback</groupId>

<artifactId>logback-core</artifactId>

<version>${logback.version}</version>

</dependency>


그리고,  resource에 logback.xml 파일을 추가하도록 한다. logback.xml은 기본적으로 logback이 사용될때, 기본적으로 load 되는 설정 xml이다. 주로 사용하는 RollingFileAppender를 이용해서 구성하도록 한다. 


logback의 Appender의 종류와 사용법들은 다음 site에서 참고하도록 한다.

http://logback.qos.ch/manual/appenders.html



<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">

<encoder>

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n

</pattern>

</encoder>

</appender>

<appender name="FILEOUT" class="ch.qos.logback.core.rolling.RollingFileAppender">

<file>c:\\Logs\\BackLog\\logFile.log</file>

<maxHistory>30</maxHistory>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>c:\\Logs\\BackLog\\logFile.%d{yyyy-MM-dd}.log</fileNamePattern>

<maxHistory>30</maxHistory>

</rollingPolicy>

<encoder>

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n

</pattern>

</encoder>

</appender>

<root level="INFO">

<appender-ref ref="STDOUT" />

<appender-ref ref="FILEOUT" />

</root>

</configuration>



이제 test를 실행하면 Console과 LogFile이 정상적으로 만들어지는 것이 확인된다. 이로서 개발전 기본 설정은 모두 마쳐지게 된다. 



Posted by Y2K
,
Controller 객체를 사용해서 각 method를 테스트하는 방법은 매우 뛰어난 테스팅 방법이지만, 불행하게도 Session 및 Identify에 대한 내용을 같이 테스팅 하는 것은 불가능하다. 특히, Session에 Wizard Data 또는 사용자에 따른 개인 정보를 저장하고 있는 경우에는 Controller 객체만을 사용하는 것은 불가능하다.

그래서, Mock을 이용해서 HttpContext를 구성하고, 구성된 HttpContext를 ControllerContext로 구성하여 Controller를 사용하게 되면 위와 같은 문제를 모두 해결 할 수 있다.


public class FakeSession : HttpSessionStateBase
{
    private readonly SessionStateItemCollection _sessionItems;
    public FakeSession(SessionStateItemCollection sessionItems)
    {
        _sessionItems = sessionItems;
    }

    public override void Add(string name, object value)
    {
        _sessionItems[name] = value;
    }

    public override int Count
    {
        get
        {
            return _sessionItems.Count;
        }
    }

    public override IEnumerator GetEnumerator()
    {
        return _sessionItems.GetEnumerator();
    }

    public override NameObjectCollectionBase.KeysCollection Keys
    {
        get
        {
            return _sessionItems.Keys;
        }
    }

    public override object this[string name]
    {
        get
        {
            return _sessionItems[name];
        }
        set
        {
            _sessionItems[name] = value;
        }
    }

    public override object this[int index]
    {
        get
        {
            return _sessionItems[index];
        }
        set
        {
            _sessionItems[index] = value;
        }
    }
    public override void Remove(string name)
    {
        _sessionItems.Remove(name);
        }
}

protected T GetContextedController(T controller, string userName, FakeSession sessionState) where T : ControllerBase
{
    //Register Route
    RouteCollection routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);

    //Build Mock HttpContext, Request, Response
    var mockHttpContext = new Moq.Mock();
    var mockRequest = new Moq.Mock();
    var mockResponse = new Moq.Mock();

    //Setup Mock HttpContext 
    mockHttpContext.Setup(x => x.Request).Returns(mockRequest.Object);
    mockHttpContext.Setup(x => x.Response).Returns(mockResponse.Object);
    mockHttpContext.Setup(x => x.Session).Returns(sessionState);

    if(string.IsNullOrEmpty(userName))
    {
        mockHttpContext.Setup(x => x.User.Identity.IsAuthenticated).Returns(false);
        mockRequest.Setup(x => x.IsAuthenticated).Returns(false);
    }
    else
    {
        mockHttpContext.Setup(x => x.User.Identity.Name).Returns(userName);
        mockHttpContext.Setup(x => x.User.Identity.IsAuthenticated).Returns(true);
        mockRequest.Setup(x => x.IsAuthenticated).Returns(true);
    }
    mockRequest.Setup(x => x.ApplicationPath).Returns("/");

    // Build Request Context
    var ctx = new RequestContext(mockHttpContext.Object, new RouteData());
    controller.ControllerContext = new ControllerContext(ctx, controller);
    return controller;
}


사용 방법은 다음과 같다.
[Test]
public void Start()
{
    _controller = new VirtualMachineController();
    SessionStateItemCollection sessionItemCollections = new SessionStateItemCollection();
    sessionItemCollections[SessionConstants.Account] = _account;
    sessionItemCollections[SessionConstants.ZoneList] = tempZoneList;

    _session = new FakeSession(sessionItemCollections);
    _controller = GetContextedController(_controller, LocalConf.AccountName, _session);
    _controller.Start("tempVmName");
}
Posted by Y2K
,
회사에서 신규 프로젝트에서 Spring.NET을 사용하기로 해서, 간단히 자료 찾아보고 공부. 끄적끄적.
개인적으로는 .NET MVC에는 Castle이 가장 어울리는 것이 아닌가 싶은데, 일단 제일 유명하고, 잘 만들어져있다는 Spring의 .NET 포팅버젼을 공부해놓는 것도 도움이 되겠다는 생각으로 시작하게 되었다.

먼저, WebService, Console Application, Web등 Context 소비자측에서 사용될 Interface를 선언한다. Interface를 통해서 구성이 되고, IoC로 구성이 될 경우 Interface의 변경은 Context 소비자의 변경을 가지고 올 수 있기 때문에 Interface의 변경에는 매우 주의를 요하게 된다.
    public interface IVirtualMachine
    {
        string CreateVM(string description, string hwProfileTextKey, string vmiName, string zoneTextKey,
            string accountName, string sequrityGroupName);
        string GetVMConfiguration(string vmName);
        string StartVM(string vmName);
        string ShutdownVM(string vmName);
        string RestartVM(string vmName);
        string StopVM(string vmName);
        string DeleteVM(string vmName);
        string SetMemorySize(string vmName, int memorySize);
        string migrateVM(string vmName, string toHostName);
        string GetVMCurrentUsage(string vmName);
        string GetPasswordData(string vmName);
    }

그리고, Interface를 상속받는 Context를 구성한다.
    public class VirtualMachine : IVirtualMachine
    {
        #region IVirtualMachine Members

        public string CreateVM(string description, string hwProfileTextKey, string vmiName, string zoneTextKey, string accountName, string sequrityGroupName)
        {
            string message = string.Format("CreateVM : {0}", description);
            return message;
        }

        public string GetVMConfiguration(string vmName)
        {
            string message = string.Format("GetVMConfiguration from Common Service : {0}", vmName);
            return message;
        }

        public string StartVM(string vmName)
        {
            throw new NotImplementedException();
        }

        public string StopVM(string vmName)
        {
            throw new NotImplementedException();
        }

        public string ShutdownVM(string vmName)
        {
            throw new NotImplementedException();
        }

        public string RestartVM(string vmName)
        {
            throw new NotImplementedException();
        }

        public string DeleteVM(string vmName)
        {
            throw new NotImplementedException();
        }

        public string SetMemorySize(string vmName, int memorySize)
        {
            throw new NotImplementedException();
        }

        public string migrateVM(string vmName, string toHostName)
        {
            throw new NotImplementedException();
        }

        public string GetVMCurrentUsage(string vmName)
        {
            throw new NotImplementedException();
        }

        public string GetPasswordData(string vmName)
        {
            throw new NotImplementedException();
        }

        #endregion
    }

Config 파일에 각 Context의 선언을 넣는다.

그리고, 사용할 소비자에서는 Spring.Context.Support.ContextRegistry 를 통해서 각 Context를 얻고, Object들을 Interface를 통해서 얻어 사용한다. 이때, 객체를 얻어와서 사용해도 되지만, 객체를 얻어오게 되면 Context의 변경이 있을때, 일관성이 떨어지고, 강한 결합으로 연결되기 때문에 n-Tier 구성에 어울리지 않게 된다. 반드시 인터페이스를 통해서 사용을 해야지 된다.

그리고, Context가 변경되게 되는 경우에는 다른 이름으로 Context를 추가하고, 정해진 Context Name을 이용해서 사용해주면 된다. 그리고, 생성자에 입력값이 있는 경우 다음과 같이 사용하면 된다.



가장 좋은 방법으로 생각되는 것은 appSettings에 사용될 Context Object Name을 추가하면 Context의 Re-Build없이 Context Object를 변경해서 사용하는 것이 가능하기 때문에 이 방법이 가장 좋을 듯 하다.



다음은 Context 소비자 코드이다.
static void Main(string[] args)
{
    string contextName = System.Configuration.ConfigurationManager.AppSettings["VirtualMachine"];
    
    IApplicationContext context = ContextRegistry.GetContext();
    IVirtualMachine vmService = (IVirtualMachine) context.GetObject(contextName);

    string result = vmService.GetVMConfiguration("YKYOON");
    Console.WriteLine(result);
}





Posted by Y2K
,
모든 입력은 위조될 수 있다. - 사용자의 입력을 신뢰하지 말아라.

변조 가능한 모든 데이터들:
  • GET QueryString
  • POST로 전송 가능한 Data
  • Cookie
  • HTTP Header에 저장된 데이터
XSS(Cross-site scripting)
: Web Application에 피해를 입히는 가장 유명하고 널리 알려진 방법
Input Message를 이용해서 Web Page에 공격자의 Form 또는 Javascript를 삽입하여 공격하는 방법

* 대응책 : 사용자가 제공한 모든 데이터는 인코딩을 통해 출력한다.

* Html.Encode(string message)를 통해 ASP .NET MVC에서 가능하다. MVC2에서는 <%: %>으로 간단히 표현 가능하다.
* ASP .NET에서 제공되는 ValidationRequest를 이용하는 방법
 - HTML, Javascript와 유사한 모든 입력을 차단한다.
 - 사용자의 입력이 매우 제한되기 때문에 추천되지 않는 방법이다.

Session Hijacking
: Session ID cookie(ASP.NET_ SessionID로 만들어진다.)를 저장하고, 자체 브라우저를 이용해서 신원을 위장 가능하다.

* 대응책
  - Client IP를 Session의 값에 같이 저장해서, Session을 발급한 Client를 재확인하는 절차를 거친다.
  - HttpOnly flag를 설정한다. 이 경우, Cookie를 javascript를 이용해서 Hijacking 하는 것이 불가능하게 된다.

CSRF
: 사용자가 정상적인 로그인을 거친 이후에 다른 사이트에 Session이 유지된 상태로 이동한 이후 타 사이트에서 값을 넘기는 것으로 사용자의 정보를 훔쳐갈 수 있다.

* 대응책
  - 사용자에게 특화된 토큰이 보안적인 요청안에 포함되도록 한다. ASP .NET MVC에서는 이러한 기법을 이미 사용하고 있다.

  <%using(Html.BeginForm()) { %>
      <%= Html.AntiForgeryToken() %>
  <%}%>

  [AcceptVerbs(HttpVerbs.Post)][ValidateAntiForgeryToken]
  public ActionResult SubmitUpdate()

SQL Injection
: SQL Query문을 Query String 또는 POST 데이터에 넣어서 데이터를 위변조한다.

* 대응책
  - 입력을 Encoding해서 방어
  - 매개변수를 사용하는 Query를 이용해서 방어
  - ORM Mapping을 이용해서 방어 (LINQ SQL, ASP .NET Entity Framework, NHibernate etc..)


Posted by Y2K
,
사용자 가입을 하는 Form의 경우에는 일반적인 Wizard Form을 취하게 된다. Wizard Form을 통해서, 사용자 정보 수집과 각 정보에 대한 Validation check를 하게 되는데, 이 때에 너무나 많은 질문으로 인하여 당황하지 않게 하는 점진적인 표현(progressive disclosure)이라는 사용자 원칙을 따른다. 모든 질문들은 전부 서로 관련이 있는 것이 아니므로 각 단계별로 소량의 질문들을 나타내는 것이다.

여러단계를 통한 이동을 하게 되는 경우, 고려되어야지 되는 것은 Next 를 통한 입력한 정보의 전달, 그리고 Back으로 돌아갔을 때 사용자의 정보의 유지가 필요하게 된다. 이를 Session으로 처리해주는 방법도 ASP .NET MVC에서는 TempData와 데이터 Serialization을 통해서 이를 쉽게 구현 가능하다.

먼저, 사용자 데이터를 LogFormatter로 Serialization시켜주기 위한 Help method와 사용자 데이터 class를 만들어준다.

[Serializable]
public class RegistrationData
{
    public string Name { get; set; }
    public string Email { get; set; }
    public int? Age { get; set; }
    public string Hobbies { get; set; }
}

public static class SerializationUtils
{
    public static string Serialize(object obj)
    {
        using(StringWriter sw = new StringWriter())
        {
            new LosFormatter().Serialize(sw, obj);
            return sw.ToString();
        }
    }

    public static object Deserialize(string data)
    {
        if(data == null)
        {
            return null;
        }
        else
        {
            return (new LosFormatter()).Deserialize(data);
        }
    }
}

그리고, Controller의 OnActionExecuting과 OnActionExecuted 함수를 다음과 같이 재 정의 한다. 이제 Controller의 모든 Action은 TempData를 검사하고, TempData에서 데이터가 있는 경우에는 Deserialization을 통해서 사용자 데이터를 가지고 온다.

public RegistrationData RegData { get; set; }

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    //Action이 call 될 때, RegData 값을 update 시켜준다.
    RegData = (SerializationUtils.Deserialize(Request.Form["regData"]) ?? TempData["regData"] ??
        new RegistrationData()) as RegistrationData;
    TryUpdateModel(RegData);
}

protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
    if(filterContext.Result is RedirectToRouteResult)
    {
        //Back이나 Next로 RedirectToAction이 호출이 되었을때, TempData에 regData값을 넣어준다.
        TempData["regData"] = RegData;
    }
}

View에서 LogFormatter를 이용해서 데이터의 ViewState를 적어준다.


    <%using(Html.BeginForm()){ %>        
        <%=Html.ValidationSummary() %>
        <%=Html.Hidden("regData", SerializationUtils.Serialize(Model)) %>
        

Name:<%=Html.TextBox("name") %>

E-Mail:<%=Html.TextBox("email")%>

<%} %>

이와 같이 구성 이후에, Html을 보면 색다른 결과가 나온다.


    

Name:

E-Mail:


그것은 VIEWSTATE를 이용해서 Wizard Form을 구성했기 때문인데, VIEWSTATE는 ASP .NET WEBFORM기술에서 악명이 높은 기술이다. 그렇지만, 이와 같이 구성이 된다면 서버의 Session상태에 구애받지 않고, 만약에 사용자가 밤새 웹브라우저를 열어놓은 일이 있더라도 값을 보장받을 수 있는 강력한 방법이 된다.

단, 주의할점이 하나 있다. VIEWSTATE 값은 Base64로 Formatting된 값이다. 이 값의 변조는 악의적인 사용자가 가능한 정보이기 때문에, 특별한 Encoding을 통하던, 아니면 다른 방법을 통해서 암호화를 하는 것이 용의하다.
Posted by Y2K
,
Data Binding : 규약을 이용하여 데이터 Entiry를 다루는 MVC Framework의 기능.

Model Binding
: HTML form의 submit시에 응용프로그램은 key/value pair로 폼의 데이터를 담고 있는 HTTP response를 받게 된다.
이때에, Http 요청 데이터를 Action method의 매개변수 및 사용자 정의 .NET 객체와 직접적으로 mapping하기 위한 알고리즘을 제공한다.

Model Binding 시의 데이터 적용 순서
1) Form(POST 매개변수 : FormCollection)
2) RouteData
3) QueryString

사용자 정의 형식에 대한 Model Binding
: 기본적으로 DefaultModelBinder는 {parameterName.PropertyName}으로 검색을 한다.
(* 대소문자는 가리지 않는다.)

다음과 같은 View가 존재한다고 할때에, 대응되는 ActionMethod는 다음과 같다.

New Product

<%=Html.ValidationSummary() %> <%using(Html.BeginForm()) { %> Name : <%=Html.TextBox("productValue.name") %>
Price : <%=Html.TextBox("productValue.price")%>
Description : <%=Html.TextBox("productValue.description")%>
<%} %>

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult New(Product productValue)
{
    try
    {
        productValue.CheckValidation();
    }
    catch(ProductException ex)
    {
        foreach(string key in ex.Keys)
        {
            ModelState.AddModelError(key, ex[key]);
        }                
        return View();
    }

    return RedirectToAction("NewResult",
        new { product = productValue });
}

BindAttribute의 이용
Bind Attribute는 Action Method의 매개변수 이름이 아닌 다른 이름으로 Binding을 원하거나, 특정 속성들이 모델 바인딩의 대상이 되어야하는지를 엄밀하게 제어할 필요가 있을 때 사용된다.

/// 
/// 다음 ActionMethod는 productA.*, productB.* 로 prefix된 view의 id를 이용해서 
/// 값을 Binding한다.
/// 
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult New([Bind(Prefix = "productA")] Product product1, 
    [Bind(Prefix = "productB")]Product product2)
{

}
/// 
/// 다음 ActionMethod는 Name과 Price를 Binding에 포함한다.
/// 
/// 
/// 
public ActionResult RegisterMember([Bind(Include = "Name, Price")] Product product)
{

}
/// 
/// 다음 ActionMethod는 DateOfBirthDay를 Binding하지 않는다.
/// 
/// 
/// 
public ActionResult DeregisterMember([Bind(Exclude = "DateOfBirthDay")] Product product)
{

}

[Bind(Include="Name")]
public class Product
{        
    public string Name { get; set; }
    public string Price { get; set; }
    public string Description { get; set; }
}

Array, Collection, Dictionary에 대한 Model Binding
: 동일한 이름을 갖는 여러개의 Textbox를 Render하는 View의 Controller는 다음과 같이 사용될 수 있다.

<%=Html.TextBox("movies") %>
<%=Html.TextBox("movies") %>
<%=Html.TextBox("movies") %>
public ActionResult DoSomething(IList movies) { throw new NotImplementedException(); }


사용자 정의 Entry의 Group에 대해서는 다른 방법이 필요하게 되는데, DefaultModelBinder는 ActionMethod의 사용에 C#의 문법과 동일한 명명 규약을 따르기를 요구한다. 다음과 같은 ActionMethod가 존재할 때에, 사용되는 View는 다음과 같다.

public ActionResult DoSomething(IList products)
{
    foreach(Product product in products)
    {
        System.Diagnostics.Debug.WriteLine(product.Name);
    }
    throw new NotImplementedException();
}


<%using(Html.BeginForm("DoSomething", "Products")) { %>
    <%for(int i = 0 ; i < 10 ; i++) { %>
        

<%=string.Format("Product : {0}", i) %>

<%--C#에서 사용되는 배열 문법과 동일하다.(products[0].Name, products[0].Price)--%> Name : <%=Html.TextBox("products["+i.ToString()+"].Name") %>
Price : <%=Html.TextBox("products["+i.ToString()+"].Price") %>
Description : <%=Html.TextBox("products["+i.ToString()+"].Description") %>
<%} %> <%} %>

사용자 지정 Model Binder의 지정
: 특정한 데이터에 대하여 사용자 지정 Model Binder를 사용하고 싶은 경우에는 IModelBinder interface를 상속한 사용자 지정 ModelBinder를 만들어주면 된다.



Model Binder가 사용되도록 구성하기 위해서 3가지 방법중 하나를 사용할 수 있다.
1) Model에 ModelBinderAttribute의 적용
[ModelBinder(typeof(XDocumentBinder))]
public class XDocument
{
    //...
}
2) ModelBinders.Binders.Add를 이용, 전역 ModelBinder에 등록하기
ModelBinders.Binders.Add(typeof(XDocument), new XDocumentBinder());
3) 매개변수를 Binding할때, Parameter attribute를 넣어주기
public ActionResult DoSomething([ModelBinder(typeof(XDocumentBinder))] XDocument xml)
{
    //...
}

.NET MVC Framework에서 ModelBinder Select 순위
1) Binding시에 명시적으로 지정된 Binder
2) 대상 형식을 위해 ModelBinders.Binders에 등록된 Binder
3) 대상 형식에 ModelBinderAttribute를 사용하여 할당된 Binder
4) DefaultModelBinder


Posted by Y2K
,
Javascript가 동작하지 않는 브라우져나 서버단에 치명적인 오류가 들어갈 수 있는 값의 경우에는 Server단에서의 Validation Check가 반드시 필요하게 된다. 전에 사용했던 IDataError interface의 경우에는 데이터 모델들의 처리가 되는데. 이는 일반적인 C#에서의 에러처리가 아닌 ASP .NET MVC에서의 Validateion 처리라고 조금은 한정지어서 생각할 수 있다.

생각해보면 C# 언어에 가장 맞는 Validation Check는 try~catch 를 이용한 방법이다. 이러한 방법을 이용해야지만이 만약 Entity를 다른 환경, 예를 들어 SilverLight에서 Entity를 표현하거나 WinForm으로 Entity를 표현할 때에 정확한 Validation Check가 가능하게 된다.

다음은 Product에 대한 Validation check code이다.

public class Product
{        
    public string Name { get; set; }
    public string Price { get; set; }
    public string Description { get; set; }

    public void CheckValidation()
    {
        ProductException exception = new ProductException();
        if(string.IsNullOrWhiteSpace(Name))
        {
            exception.Add("name", "Name is empty");
        }

        if(string.IsNullOrWhiteSpace(Description))
        {
            exception.Add("description", "Description is empty");
        }

        if(string.IsNullOrEmpty(Price))
        {
            exception.Add("Price", "Price is empty");
        }

        if(!exception.IsValid)
        {
            throw exception;
        }
    }
}

public class ProductException : Exception, IDictionary
{
    public bool IsValid { get { return _exceptMessages.Count == 0; } }
    private Dictionary _exceptMessages;
    public ProductException()            
    {
        _exceptMessages = new Dictionary();
    }

    #region IDictionary Members

    public void Add(string key, string value)
    {
        _exceptMessages.Add(key, value);
    }

    public bool ContainsKey(string key)
    {
        return _exceptMessages.ContainsKey(key);
    }

    public ICollection Keys
    {
        get { return _exceptMessages.Keys; }
    }

    public bool Remove(string key)
    {
        return _exceptMessages.Remove(key);
    }

    public bool TryGetValue(string key, out string value)
    {
        return _exceptMessages.TryGetValue(key, out value);
    }

    public ICollection Values
    {
        get { return _exceptMessages.Values; }
    }

    public string this[string key]
    {
        get
        {
            return _exceptMessages[key];
        }
        set
        {
            _exceptMessages[key] = value;
        }
    }

    #endregion

    #region ICollection> Members

    public void Add(KeyValuePair item)
    {
        _exceptMessages.Add(item.Key, item.Value);
    }

    public void Clear()
    {
        _exceptMessages.Clear();
    }

    public bool Contains(KeyValuePair item)
    {
        return _exceptMessages.Contains(item);
    }

    public void CopyTo(KeyValuePair[] array, int arrayIndex)
    {
        throw new NotImplementedException();
    }

    public int Count
    {
        get { return _exceptMessages.Count; }
    }

    public bool IsReadOnly
    {
        get { throw new NotImplementedException(); }
    }

    public bool Remove(KeyValuePair item)
    {
        return _exceptMessages.Remove(item.Key);
    }

    #endregion

    #region IEnumerable> Members

    public IEnumerator> GetEnumerator()
    {
        return _exceptMessages.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _exceptMessages.GetEnumerator();
    }

    #endregion
}
Product을 사용하는 Controller의 예
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult New(Product productValue)
{
    try
    {
        productValue.CheckValidation();
    }
    catch(ProductException ex)
    {
        foreach(string key in ex.Keys)
        {
            ModelState.AddModelError(key, ex[key]);
        }                
        return View();
    }

    return RedirectToAction("NewResult",
        new { product = productValue });
}

Posted by Y2K
,
View는 WebApplication의 Request가 Response로 바뀌는 BlockBox가 된다.

MVC에서 View는 완전한 출력을 담당하는 책임을 가지고 있다. 단지 Controller의 출력만을 담당하며, 간결한 표현 로직을 사용하여 그 출력을 최종 HTML로 Render하는 임무를 가진다. 그렇지만, 표현로직과 업무로직사이의 경계는 매우 모호하다. 업무로직과 표현 로직에 대한 것은 조금은 주관적이라고 생각이 되기 때문에.. 최대한 Controller에서 동작을 하고 표현 부분에 대해서는 로직의 최대한도로 줄이는 것을 목표로 하는 것이 가장 좋다.
또한, View의 문제는 Test에 있다. Html을 테스트하는 것은 GUI 프로그램에서 사용자 액션을 테스트하는 것과 동일하다. 테스트 할 방법도 마땅하지 않으며, 표시된 HTML을 TEXT단위로 테스트 하는 것은 공백 문제부터 시작해서 많은 어려움이 있기 때문에, 많은 책들의 저자와 비슷하게 VIEW는 봐서 잘 되는 것이 최고다. 라는 생각이 든다. 딱히 테스트 할 방법도 안보이는 것이 사실이다.;

WebForm View Engine
: MVC Framework는 WebFormViewEngine이라는 class로 구성된 View Engine을 사용하고 있다. 이 ViewEngine은 기존의 ASP .NET WebForm Engine을 기반으로 구축되어있다. 그러나 WebFormViewEngine에서 View는 단순한 HTML Template이다. 이는 Controller에서 받은 특정 데이터인 ViewData와 강력히 형식화된 Model 객체를 기반으로 만들어진 HTML 문자를 표현하는 기능만을 가지게 된다. 또한 내부적으로 사용되는 기반 기술은 ASP .NET WebForm 서버 페이지 기술과 동일하다.만약에 자체적인 View Engine을 만들고 싶다면, IViewEngine과 IView interface를 상속받아 구현하면 된다.

View의 데이터 전송
: ViewData라는 Dictionary<string, object> 형식의 데이터 또는 강력한 형식의 Model을 이용하는 방법이 있다. 두 방법의 차이는 Controller에서는 전혀 알 필요가 없다. 단지 Controller에서 데이터 형식의 Model을 전송해서 View로 전달 될때, 강력한 Model 형식의 View의 경우에는 형변환이 가능한지만을 알아보게 된다. 따라서 이 방법은 매우 유용한 것이 된다.

ViewData.Eval()의 이용
: ViewData.Eval()은 대소문자를 가리지 않고, 값이 없는 경우에 String.Empty를 리턴하는 사용하기 편한 함수이다. 또한 Eval함수는 .으로 구분되는 토큰 명명법을 사용하고 있기 때문에 Model의 속성또한 표현할 수 있다. ViewData에 key를 Name으로 넣었는지, name으로 넣었는지. 아니면 값이 없는 경우에 NULL처리를 해주는 것을 빼먹지 않았는지 신경쓰지 않고 사용하기에 매우 좋은 함수이다. 또한 Html Helper method에서 Html.TextBox("comment")를 사용하는 경우, 이 코드는 다음과 같다. <input name="comment" id="comment" type="text" value="<%=Html.Encode(ViewData.Eval("comment"))%>"/>. 편하게 작성도 가능하고, 값이 없는 경우에 나오는 모든 에러처리를 해주기 때문에 간결한 코드 작성이 가능하다.
Posted by Y2K
,
모든 Controller는 IController interface를 구현한다.

* Mvc.Controller의 특징
1. ActionMethods
  : Controller의 동작이 여러개의 method로 분할된다. 각 Action method는 서로다른 URL로 노출되며,
  들어오는 요청에서 추출된 매개변수를 가지고 있다.
2. ActionResult
  : Action의 의도된 결과를 나타내는 개체를 선택해서 반환할 수 있다. 결과를 지정하는 것과 실행하는
  것이 분리되기 때문에 자동화된 테스트가 상당히 간단하게 구성될 수 있다.
3. Filter
  : 재사용 가능한 동작들을 Filter로 캡슐화할 수 있다.

* Controller의 입력 처리
Mvc.Controller에서 사용 가능한 속성들
1. Request.QueryString : 요청과 함께 전송된 GET 변수들
2. Request.Form : 요청과 함께 전송된 POST 변수들
3. Request.Cookie : 요청과 함께 전송된 Cookie
4. Request.HttpMethod : 요청을 위해 사용된 Http method
5. Request.Headers : 요청과 함께 전송된 HTTP Header
6. Request.Url : 요청된 Url
7. Request.UserHostAddress : 요청을 보내온 사용자의 Ip Address
8. RouteData.Route : 요청에 대해 선택된 RouteTable.Routes Entry
9. RouteData.Values : 현재의 Route 매개 변수(URL에서 추출된 값이거나 기본 값)
10. HttpContext.Application : Application state 저장소
11. HttpContext.Cache : Application cache 저장소
12. HttpContext.Items : 현재 요청에 대한 상태저장소
13. User : 로그인 한 사용자의 인증정보
14. TempData : Session 내에서 이전 HTTP 요청을 처리하는 동안에 저장된 임시 Data

* Controller의 출력 처리
a. View를 Rendering하는 것으로 HTML 반환
b. HTTP 재전송 호출(311, 312 code 반환)
c. 응답의 출력 스트림에 다른 데이터 전송(xml, json, file)

* Action method 안에서의 직접적으로 Response를 다루는 일은 절대로 하지 않아야지 된다. 대신에 특정한 응답을 나타낼 수 있는 ActionResult를 상속한 객체를 반환하도록 한다.

* ActionResult 형식들
1. ViewResult : 지정된 View Template이나 기본 View Template을 Rendering 한다. [Mvc.Controller.View() method]
2. PartialViewResult : 지정된 Partial View Template이나 기본 Partial View Template을 Rendering한다.(Mvc.Controller.PartialView()]
3. RedirectToRouteResult : Routing 구성 설정에 따라 URL을 생성하면서 302 재전송을 하게 한다.[Mvc.Controller.RedirectToAction()]
4. RedirectResult : 임의의 URL로 HTTP 302 재전송을 하게 한다. RedirectToRouteResult와 다른 점은 RedirectResult는 외부의 URL을 사용가능하다는 차이점을 가지고 있다.
5. ContentResult : 브라우저로 text 데이터를 반환할 수 있다. 선택적으로 Content Header를 지정 가능하다.
6. FileResult : 이진 데이터를 브라우저로 직접 전달한다.
7. Jsonresult : .NET 객체를 Json 포멧으로 직렬화하고 이를 응답으로 전송한다.
8. JavascriptResult : 브라우저에 의해 실행될 수 있는 javascript 소스코드의 일부를 전송한다.
9. HttpUnauthorizedResult : Http 응답코드를 401로 설정한다.
10. EmptyResult : 아무 일도 하지 않는다.

사용자 지정 ActionResult를 만들고 싶은 경우(Image를 가공해서 반환한다던지 등의 Action이 가능하다)에는 ActionResult를 상속받아서 ExecuteResult를 재정의해서 사용하면 된다. 다음은 그 예제 코드이다.


public class WatermarkedImageResult : ActionResult
{
    public string ImageFileName { get; private set; }
    public string WatermarkText { get; private set; }

    public WatermarkedImageResult(string imageFileName, string watermarkText)
    {
        ImageFileName = imageFileName;
        WatermarkText = watermarkText;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        using(var image = Image.FromFile(ImageFileName))
        using(var graphics = Graphics.FromImage(image))
        using(var font = new Font("Arial", 10))
        using(var memoryStream = new MemoryStream())
        {
            var textSize = graphics.MeasureString(WatermarkText, font);
            graphics.DrawString(WatermarkText, font, Brushes.White, 10, image.Height - 10 - textSize.Height);

            image.Save(memoryStream, ImageFormat.Png);
            var response = context.RequestContext.HttpContext.Response;
            response.ContentType = "image/png";
            response.BinaryWrite(memoryStream.GetBuffer());
        }
    }
}

* TempData와 Session과의 비교
기본적으로 TempData의 저장소는 Session이 된다. 그렇기 때문에 TempData를 사용하고 싶으면 반드시 Session을 enable 시켜야지 된다. 하지만 TempData의 고유한 특징은 이것이 매우 작은 데이터라는 것이다. 각 entry는 오직 한번의 이전 요청을 저장하고 난 다음 없어진다. 처리 후에 자동으로 청소가 되니 RedirectToAction()에 걸쳐 개체를 유지하는데 적합하다.

* 재 사용가능한 Filter 처리하기
Filter를 이용해서 Controller와 ActionMethod에 추가적인 동작을 지정할 수 있다.
Filter는 요청 처리 PipeLine에 별도의 단계를 추가하는 .NET Attribute로 Action method의 실행 전후에, ActionResult가
실행 전후에 추가적인 로직을 삽입할 수 있다.

Action method의 실행 전후는 IController.Execute method의 실행 전, 후를 의미하며,
ActionResult의 실행 전후는 ActionResult.ExecuteResult method의 실행 전, 후를 의미한다.
따라서 실행 순서는 OnActionExecuting, OnActionExecuted, OnResultExecuting, OnResultExecuted 순서로 실행된다.

ex) HandleError : HandleErrorAttribute는 Controller의 Action method가 실행될 때에 예외가 발생되면 HandleError에 지정된 View가 Render된다.이때에 web.config에 customError mode가 on이 되어있어야지 된다. 이때 지정된 View는 HandleErrorInfo data model이 전달되며, 이는 불려진 Controller와 ActionName, Exception의 정보를 담게 된다. 이 상태는 Error를 처리된 상태로 만들게 되며 따라서 Http 응답코드는 에러가 아닌 200이전달되게 된다.


Controller Test Code
* Controller의 Unit Code는 의미있는 단위 테스트를 작성하기 위해서 많은 사람들이 AAA Pattern을 따른다. Arrange-Act-Assert로 이루어지는
이 방법은 대부분의 Controller 테스트에 유용하다.

[TestClass]
public class HomeControllerTest
{
    [TestMethod]
    public void CheckViewName()
    {
        HomeController controller = new HomeController();
        ViewResult viewResult = controller.Index();

        Assert.IsNotNull(viewResult);
        Assert.AreEqual(viewResult.ViewName, "index");
    }

    [TestMethod]
    public void CheckViewData()
    {
        HomeController controller = new HomeController();
        ViewResult viewResult = controller.Index();

        Assert.IsNotNull(viewResult);
        Assert.AreEqual(6, viewResult.ViewData["age"]);
    }

    [TestMethod]
    public void CheckRedirection()
    {
        HomeController controller = new HomeController();
        RedirectToRouteResult result = controller.Index();

        Assert.IsNotNull(result);
        Assert.AreEqual("RedirectActionName", result.RouteValues["action"]);
    }
}
Posted by Y2K
,